-
Notifications
You must be signed in to change notification settings - Fork 213
Extension type dispatch. #3349
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
Sounds right: In It doesn't matter which actual type argument is passed (if you're passing For
Not quite: An extension type can be a useful type argument in the case where it is used multiple times in the signature: extension type Inch(double value) implements double {}
extension on double { Inch get inch => Inch(this); }
X f<X extends double>(X x) {
print('Floor: ${x.floor()}');
return x;
}
void main() {
var length = 1.5.inch;
var otherLength = f(length);
// bool _ = otherLength; // Shows that `otherLength` has type `Inch`.
}
I think the concise way to characterize the situation is as follows: If you need OO dispatch then you need to use a reified type (and extension types are not reified, they are erased to the underlying representation type). |
@eernstg do you think that it is possible to declare some void main() {
// run uses static dispatch for SomeExtensionType.foo
run<SomeExtensionType>(
// ...
);
// run uses OO dispatch for SomeClass.foo
run<SomeClass>(
// ...
);
}
void run<T>() {
// T has a member called foo
}
extension type SomeExtensionType(double foo) {}
abstract class SomeClass {
double get foo;
String get bar;
} I suppose some form of monomorphization would be required here to make this work, correct? And since there doesn't appear to be much support for monomorphization (dart-lang/site-www#4998), this is not possible yet? |
I wrote a micro benchmark to compare how the different runtimes behave. (Note: extension types have not shipped yet, so the numbers here are not final) JS:
Dart SDK version: 3.2.0-134.1.beta (beta) (Thu Sep 14 06:41:14 2023 -0700) on "macos_arm64" JIT:
AOT:
Code: void main() {
const size = 10000000;
final datasetClass = List.generate(size, (final a) => SomeClass(foo: a));
final datasetExtensionType = List.generate(size, (final a) => SomeExtensionType(a));
final sw = Stopwatch();
final viaClass = RunClass();
final viaExtensionType = RunExtensionType();
sw.start();
viaClass.execute(datasetClass);
sw.stop();
print("via class on class: ${sw.elapsedMilliseconds}ms");
sw.reset();
sw.start();
viaExtensionType.execute(datasetExtensionType);
sw.stop();
print("via extension type on class: ${sw.elapsedMilliseconds}ms");
sw.reset();
sw.start();
run<SomeClass>(datasetClass, (final a) => a.foo);
sw.stop();
print("via class on function: ${sw.elapsedMilliseconds}ms");
sw.reset();
sw.start();
run<SomeExtensionType>(datasetExtensionType, (final a) => a.foo);
sw.stop();
print("via extension type on function: ${sw.elapsedMilliseconds}ms");
sw.reset();
sw.start();
runClass(datasetClass);
sw.stop();
print("via class on function monomorphic: ${sw.elapsedMilliseconds}ms");
sw.reset();
sw.start();
runExtensionType(datasetExtensionType);
sw.stop();
print("via extension type on function monomorphic: ${sw.elapsedMilliseconds}ms");
}
class RunExtensionType with Run<SomeExtensionType> {
@override
int sum(final SomeExtensionType v) => v.foo;
}
class RunClass with Run<SomeClass> {
@override
int sum(final SomeClass v) => v.foo;
}
extension type SomeExtensionType(int foo) {}
class SomeClass {
final int foo;
const SomeClass({
required this.foo,
});
}
mixin Run<T> {
int sum(
final T v,
);
int execute(
final List<T> tree,
) {
int total = 0;
for (final a in tree) {
total += sum(a);
}
return total;
}
}
int run<T>(
final List<T> data,
final int Function(T) sum,
) {
int total = 0;
for (final a in data) {
total += sum(a);
}
return total;
}
int runClass(
final List<SomeClass> data,
) {
int total = 0;
for (final a in data) {
total += a.foo;
}
return total;
}
int runExtensionType(
final List<SomeExtensionType> data,
) {
int total = 0;
for (final a in data) {
total += a.foo;
}
return total;
} Unfortunately, there's no way to automatically monomorphize code. The manual monomorphization appears to be consistently the fastest and it would be great if Dart could offer an automated way for doing that. Furthermore, injected "delegates" in the form of functions are very convenient, but appear not to be as efficient as having a class where the functions are methods. #1048 + some form of configurable monomorphization (dart-lang/sdk#52722 (comment)) might support making the convenient path as efficient as the class-based implementation. |
The extension-type-related performance regression observed in the benchmark above has been fixed via dart-lang/sdk#53542 (comment) There are several dimensions one can play with here:
I am hopeful that the last dimension, that I did not consider before because it is a fairly new addition to Dart, could solve this issue in a convenient way. However, under AOT there appears to be a major performance regression that makes everything else 10x slower once that feature has been used. I'm going to file an issue for that tomorrow. Furthermore, it's weird that under JIT, the manually monomorphized version appears to perform WORSE than a delegate + function + |
@modulovalue wrote:
True, it is not possible (at least not plausible) to perform static analysis of the body of The only plausible way to get that semantics, as far as I can see, would be to copy the declaration of However, that's a really awkward match for Dart, because it is in general not decidable which values any given type variable in Dart will have at run time. Also, there is no guarantee that the number of distinct values for a given type variable is limited by any finite number. So we'd want a "smart" approach where we generate just one run-time function for "most cases" (where the generated code is valid for all of them), and then a few extras for cases where the compiled code must be different. (And then we'd need to consider what it means to call that function dynamically, or tearing it off in a location where no context type is available, etc.) You could get this kind of behavior by declaring This discussion is very interesting, and it is important that we're aware of the performance characteristics of different "styles" of coding. However, at the same time I can't help thinking that there is a very serious built-in conflict at play here: (1) We want to know all facts at compile time in order to generate code that runs fast, and (2) we want to write code which is generic in order to be able to use it with a large number of different factual situations. "Just duplicate the generic code and specialize it for each call site" is an obvious answer, but it does also have a number of drawbacks (like code size explosion, and questionable readability). It does make sense to try to find the golden mean path here, but we shouldn't be too surprised if the trade-offs appear to be hard. ;-) |
Thank you, @eernstg. Your remark about global functions being constant (#1048 (comment)) gave me the idea to try the I'm going to wait until the following issues are resolved before I draw any further conclusions: |
(This issue is motivated by the idea to have support for augmented maps that are general and efficient. I'll be referring to simple trees to keep things simple.)
Consider, for example, a simple tree data structure with extra "augmented" data.
The tree might be big, so we want the augmented data to be represented in an efficient way.
ThirdPartyAug
), and with a function that expects aTree
where AUG needs to be aThirdPartyAug
(asrun
)Tree
).Here's what that might look like:
If we wanted to make
run
support custom augmentations, then it seems like we would be forced not to bound AUG to ThirdPartyAug, but to provide the interface that we want explicitly, e.g.:In short:
does that mean that type parameters bounded to an extension type are "useless"? (If the bound is the only type that it can ever be instantiated with, then why should the type parameter exist in the first place?)
do we have to choose between efficiency (
run
) and generality (run2
)? I suspect that callingsum
via a level of indirection would be much less efficient than calling it directly by giving AUG a bound, or can we be general and maximally efficient at the same time?The text was updated successfully, but these errors were encountered: