Skip to content

[extension types] extension types slower than classes on AOT. #53572

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

Closed
modulovalue opened this issue Sep 20, 2023 · 3 comments
Closed

[extension types] extension types slower than classes on AOT. #53572

modulovalue opened this issue Sep 20, 2023 · 3 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-performance Issue relates to performance or code size

Comments

@modulovalue
Copy link
Contributor

Consider how an extension type is slower than a plain old class on AOT (7ms vs 9ms) in the micro benchmark at the bottom of this issue description.

I expected extension types to be at least as fast as classes, but not slower.

Note: the build that this ran on includes the following fix #53542 (comment), however, extension types appear to still be slower than a plain old class.

Dart SDK version: 3.2.0-edge.88b07ba64e07fb0eef9c777769917c65fdf1832e (be) (Tue Sep 19 21:57:39 2023 +0000) on "macos_arm64"

AOT

dart compile exe --enable-experiment=inline-class dispatch_bench.dart

./dispatch_bench.exe

via class • on class with instantiated mixin: 7ms
via extension type • on class with instantiated mixin: 9ms

JIT

dart --enable-experiment=inline-class dispatch_bench.dart

via class • on class with instantiated mixin: 25ms
via extension type • on class with instantiated mixin: 23ms
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();
  void measure(
    final String name,
    final void Function() fn,
  ) {
    sw.reset();
    sw.start();
    fn();
    sw.stop();
    print("$name: ${sw.elapsedMilliseconds}ms");
    
  }
  final viaClass = RunClass();
  final viaExtensionType = RunExtensionType();
  measure(
    "via class • on class with instantiated mixin",
    () => viaClass.execute(datasetClass),
  );
  measure(
    "via extension type • on class with instantiated mixin",
    () => viaExtensionType.execute(datasetExtensionType),
  );
}

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) {
  static int fooSelector(
    final SomeExtensionType a,
  ) => a.foo;
}

class SomeClass {
  static int fooSelector(
    final SomeClass a,
  ) => a.foo;
  
  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;
  }
}
@lrhn lrhn added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-performance Issue relates to performance or code size labels Sep 20, 2023
@a-siva
Copy link
Contributor

a-siva commented Sep 20, 2023

//cc @alexmarkov

@alexmarkov
Copy link
Contributor

This is not a fair comparison: extension type is erased to an int, while class holds int in a field.
The following improved example shows that performance of extension type matches the performance of bare int:

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 datasetInt = List.generate(size, (final a) => a);
  final sw = Stopwatch();
  void measure(
    final String name,
    final void Function() fn,
  ) {
    sw.reset();
    sw.start();
    fn();
    sw.stop();
    print("$name: ${sw.elapsedMilliseconds}ms");
    
  }
  final viaClass = RunClass();
  final viaExtensionType = RunExtensionType();
  final viaInt = RunInt();
  measure(
    "via class • on class with instantiated mixin",
    () => viaClass.execute(datasetClass),
  );
  measure(
    "via extension type • on class with instantiated mixin",
    () => viaExtensionType.execute(datasetExtensionType),
  );
  measure(
    "via int • on class with instantiated mixin",
    () => viaInt.execute(datasetInt),
  );
}

class RunExtensionType with Run<SomeExtensionType> {
  @override
  int sum(final SomeExtensionType v) => v.foo;
}

class RunInt with Run<int> {
  @override
  int sum(int v) => v;
}

class RunClass with Run<SomeClass> {
  @override
  int sum(final SomeClass v) => v.foo;
}

extension type SomeExtensionType(int foo) {
  static int fooSelector(
    final SomeExtensionType a,
  ) => a.foo;
}

class SomeClass {
  static int fooSelector(
    final SomeClass a,
  ) => a.foo;
  
  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;
  }
}

After inlining, this micro-benchmark effectively reduces to a calculation a sum of elements of a list. int elements of a list are represented as boxed instances (because List can contain arbitrary objects), and each element is conditionally unboxed before addition. This conditional unboxing happens both for extension type and bare int. In case of a class, field SomeClass.foo is always unboxed which saves one bit test and a branch. As it is a very small micro-benchmark which doesn't do anything useful, the extra test and branch (while being pretty cheap) still affects benchmark result.

So this benchmark shows that holding int values in fields of classes could be more efficient than holding them in general-purpose lists, and doesn't show anything about extension types. I can also recommend using typed lists (such as Int64List from dart:typed_data) to store elements in the unboxed form instead of using general-purpose List if you need to handle a lot of int data.

As far as I can see this example works as expected, so closing the issue.

@modulovalue
Copy link
Contributor Author

@alexmarkov thank you for looking into this. The point of this benchmark was to validate the following hypothesis:

Values wrapped in an extension type are always "faster" than values wrapped in a class.

This doesn't seem to be the case, but it has nothing to do with extension types as I can confirm that extension types are as fast an plain integers on my machine. I apologize for the false alarm!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-performance Issue relates to performance or code size
Projects
None yet
Development

No branches or pull requests

4 participants