Description
Hi Dart folks, long time no see.
Take this as an RFC... arguably math.Point is working exactly as implemented and nothing I'm going to say will be a major surprise, at least qualitatively....
I've always wondered if math.Point isn't a pathological use-case for reified generics? In my head, reified generics work great for cases where you have a lot of generic code (where you wanna pay the cost to binary size once) dealing with mostly opaque objects like with containers. However, math.Point is the polar opposite. On top, the operations it implements are extremely cheap lending exacerbated significance to the virtual function overhead that comes with reified generics.
I finally came around to quantify the impact with a silly micro-benchmark (never trust a micro-benchmark in general and don't trust me specifically) and found the overhead for simple vector additions to be in the order of 20+x [1].
Motivated by that result I replaced math.Point with my own basic points in some number-crunching heavy libraries and got real-life performance improvements of tens or hundreds of percent. Nothing to sneeze at.
So my question to you, is math.Point (and probably math.Rectangle as well) setting a bad example? By virtue of being in the standard library, it's the go-to solution for many mathy dart libraries. Unless you want to write code that specifically deals with Point<num>
(and all the type assertion exceptions that come with it), there isn't much benefit in math.Point being generic. My hunch is that most users would happily pay the cost to binary size for an unrolled version given the significant performance advantage. I would even argue that something as frame-time-sensitive as Flutter would greatly benefit from a pivot.
I understand that removing math.Point won't fly w/o breaking the world but maybe it's worth warning/steering users to an unrolled alternative?
Curious to hear what you think.
Cheers,
Sebastian
[1] my micro-benchmark:
import 'dart:math';
class PointInt {
final int x;
final int y;
const PointInt(this.x, this.y);
PointInt operator +(PointInt other) =>
PointInt(x + other.x, y + other.y);
}
class PointDouble {
final double x;
final double y;
const PointDouble(this.x, this.y);
PointDouble operator +(PointDouble other) =>
PointDouble(x + other.x, y + other.y);
}
const N = 1000000000;
final ints = <(String, Function)>[
('PointInt', () {
var x = PointInt(23, 23), y = PointInt(42, 42);
for (int i = 0; i < N; ++i) x += y;
}),
('Point<int>', () {
var x = Point<int>(23, 23), y = Point<int>(42, 42);
for (int i = 0; i < N; ++i) x += y;
}),
];
final doubles = <(String, Function)>[
('PointDouble', () {
var x = PointDouble(23, 23), y = PointDouble(42, 42);
for (int i = 0; i < N; ++i) x += y;
}),
('Point<double>',
() {
var x = Point<double>(23, 23), y = Point<double>(42, 42);
for (int i = 0; i < N; ++i) x += y;
}),
];
void main() {
for (final suite in [ints, doubles]) {
final results = suite.map((s) {
final (k, f) = s;
final w = Stopwatch()..start();
f();
return (k, w.elapsed);
});
final factor =
results.last.$2.inMicroseconds / results.first.$2.inMicroseconds;
print('${results}: ${factor.toStringAsFixed(1)}');
}
}
and example output:
$ dart compile exe generic2.dart && ./generic2.exe
Generated: /home/sebastian/projects/dart/generic2.exe
((PointInt, 0:00:00.335539), (Point<int>, 0:00:08.149218)): 19.1
((PointDouble, 0:00:00.284572), (Point<double>, 0:00:09.913140)): 23.1