-
Notifications
You must be signed in to change notification settings - Fork 1.7k
The generic implementation of math.Point
entails significant overhead. Should there be an alternative or warning?
#53912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
That's a very interesting observation! Note the relations to monomorphization, e.g., issue #52722, and other issues that @modulovalue created or commented on. Perhaps macros could offer compact code (avoiding two or more copies of essentially the same code at the source level) with type specific variants (e.g., |
@ignatz if you want backwards compatibility with the Point interface, you could try subclassing Point from dart:math like this: 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);
}
class PointDouble2 extends Point<double> {
const PointDouble2(super.x, super.y);
PointDouble2 operator +(covariant PointDouble2 other) =>
PointDouble2(x + other.x, y + other.y);
}
class PointInt2 extends Point<int> {
const PointInt2(super.x, super.y);
PointInt2 operator +(covariant PointInt2 other) =>
PointInt2(x + other.x, y + other.y);
}
const N = 10000000;
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;
}),
('PointInt2', () {
var x = PointInt2(23, 23), y = PointInt2(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;
}),
('PointDouble2',
() {
var x = PointDouble2(23, 23), y = PointDouble2(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)}');
}
} I've tested it on DartPad (other runtimes might vary) and it appears to provide the performance that you're looking for:
A self type would be nice for getting rid of the use of covariant here. It looks like other languages have annotations for doing this in an automated way. For performance sensitive use cases there's a first party vector_math package that avoids the use of generics. |
Thanks Erik, thanks Modestas for the super quick reply. Let me take a step back, I feel like we're mixing two conversations:
Personally, I'm not very interested in (2.) unless it comes shipped as part of the standard library and with clear guidance to create momentum. Implementing your own Point class isn't very hard or verbose. However, getting folks to use yours or having to deal with the glue for 50 different Point implementations throughout your program's transitive closure of dependencies is hard. Let me just quickly comment on the proposed inheritance-based solution. It is a fun little detail that inheritance forces monomorphization (guaranteed or not). I will certainly remember this trick. Compiling to native, I'm seeing the projected performance gains, however in "compat" mode: Point<int> x = PointInt2(23, 23);
Point<int> y = PointInt2(42, 42);
for (int i = 0; i < N; ++i) x += y; I'm seeing a 12x performance reduction over plain Coming back to (1.), before-mentioned annotations (et al) would arguably be a silver-bullet: huge performance gains w/o any additional work besides re-compiling. I'm keen to hear from you how attainable such a solution would be in the short/mid/long-term? Depending on the time-horizon, maybe there's still an opportunity to at least create awareness one way or another? |
The rectangle being defined in terms of I'd be happy to move the class into Deprecating it entirely and throwing it away would also be great. Probably hard to sell, since there are uses. Deprecating the class and replacing it with an Still, removing, replacing, or even changing, platform library types is hard, because there is no versioning or opt-out for users. Their code gets the new version when their users upgrade their SDK. |
I don't see your proposals as mutually exclusive. Even if the end goal was to throw it away eventually, you'd need to offer alternatives and leave plenty of lead time first.
Marking I had a quick look at extension types, it's not clear to me if one even wanted to tie shiny new alternatives to Hope you folks will have a chance to talk this out. I don't feel very strong, at least not yet, towards any solution. In the end it would just be lovely, to see fewer libraries grab for Thanks y'all for being so responsive and on top of things. |
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:
and example output:
The text was updated successfully, but these errors were encountered: