Skip to content

AOT: int.toDouble() in a hot loop is 5x slower than the same conversion written as 0.0 + x #64209

Open

Description

@krll-kov

acc += x.toDouble() and acc += x compute the same thing. In JIT they take the same time. In AOT the first is 5x slower.

The practical damage is to SIMD over integer data: Float32x4 takes doubles, so building one from a Uint8List has to convert, and that makes the vector code slower than the scalar loop it was meant to replace.

import 'dart:typed_data';

const n = 1024;
const iters = 200000;

double explicitConversion(Uint8List a) {
  var acc = 0.0;
  for (var i = 0; i < a.length; i++) {
    acc += a[i].toDouble();
  }
  return acc;
}

double implicitPromotion(Uint8List a) {
  var acc = 0.0;
  for (var i = 0; i < a.length; i++) {
    acc += a[i];
  }
  return acc;
}

double simdExplicit(Uint8List a) {
  var acc = Float32x4.zero();
  for (var i = 0; i < a.length; i += 4) {
    acc += Float32x4(
      a[i].toDouble(),
      a[i + 1].toDouble(),
      a[i + 2].toDouble(),
      a[i + 3].toDouble(),
    );
  }
  return acc.x + acc.y + acc.z + acc.w;
}

double simdPromoted(Uint8List a) {
  var acc = Float32x4.zero();
  for (var i = 0; i < a.length; i += 4) {
    acc += Float32x4(
      0.0 + a[i],
      0.0 + a[i + 1],
      0.0 + a[i + 2],
      0.0 + a[i + 3],
    );
  }
  return acc.x + acc.y + acc.z + acc.w;
}

void main() {
  final a = Uint8List(n);
  for (var i = 0; i < n; i++) {
    a[i] = (i * 37) & 0xff;
  }
  // All four compute the same sum.
  print(
    'results: ${explicitConversion(a)} ${implicitPromotion(a)} '
    '${simdExplicit(a)} ${simdPromoted(a)}',
  );

  var sink = 0.0;
  void bench(String label, double Function() f) {
    var best = 1e9;
    for (var r = 0; r < 6; r++) {
      final sw = Stopwatch()..start();
      for (var i = 0; i < iters; i++) {
        sink += f();
      }
      sw.stop();
      final dt = sw.elapsedMicroseconds / 1e6;
      if (dt < best) best = dt;
    }
    print(
      '${label.padRight(30)}${(best * 1000).toStringAsFixed(1).padLeft(7)} ms',
    );
  }

  bench('acc += x.toDouble()', () => explicitConversion(a));
  bench('acc += x', () => implicitPromotion(a));
  bench('Float32x4(x.toDouble(), ...)', () => simdExplicit(a));
  bench('Float32x4(0.0 + x, ...)', () => simdPromoted(a));
  if (sink == -1) print('never');
}

All four print 130560.0, so they do the same arithmetic.

dart compile exe repro.dart, best of 6, macos_arm64:

3.13.3 AOT dev AOT 3.13.3 JIT dev JIT
acc += x.toDouble() 664.1 ms 676.1 ms 127.1 ms 126.5 ms
acc += x 127.9 ms 130.6 ms 127.0 ms 128.8 ms
Float32x4(x.toDouble(), ...) 413.3 ms 405.4 ms 97.8 ms 98.7 ms
Float32x4(0.0 + x, ...) 78.7 ms 78.5 ms 74.1 ms 74.7 ms

Not specific to Float32x4 or to float32: Float32List[i] = x.toDouble() costs ~480 ms against ~155 ms for storing a constant, and Float64List is the same.

Vectors themselves are in good shape - arithmetic over a populated Float32x4List beats the scalar loop by 2.5x in AOT. The whole loss is on the way in. Concretely, a Float32x4 sum of squared differences over a 16x16 byte block is 3x slower than scalar in AOT with toDouble(), and 14% faster with 0.0 + x.

Workaround: write 0.0 + x instead of x.toDouble().

#46182 asks for a Float32x4 constructor taking ints for exactly this reason ("avoiding 4 calls to i.toDouble()"). That would cover the SIMD case, but the slowdown is not limited to it - any toDouble() in a hot AOT loop pays it.

Tested on 3.13.2 stable, 3.13.3 stable and 3.14.0-180.0.dev, macos_arm64. Not tested on x64.

Activity

  1. krll-kov commented on Sep 4, 2026

    @krll-kov
    Author

    For triage: same symptom as #63217, which was closed when cl/497000 added AOT specialization for the Int32x4 operators. This is a different operation and shows up outside SIMD as well, so it is not that one coming back.

    Possibly the same shape, though: _IntegerImplementation.toDouble is in OTHER_RECOGNIZED_LIST but not in GRAPH_INTRINSICS_LIST or ASM_INTRINSICS_LIST, so it stays a call in AOT where there is no IC feedback to specialize it. That is what cl/497000 describes for the Int32x4 operators "never added to the recognized-method list as graph intrinsics … AOT has no IC feedback and therefore fell back to boxed calls" and those five are in GRAPH_INTRINSICS_LIST now.

  2. modulovalue commented on Sep 4, 2026

    @modulovalue
    Contributor
  3. added
    area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.
    on Sep 16, 2026
  4. added
    P3A lower priority bug or feature request
    type-performanceIssue relates to performance or code size
    triagedIssue has been triaged by sub team
    on Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3A lower priority bug or feature requestarea-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.triagedIssue has been triaged by sub teamtype-performanceIssue relates to performance or code size

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions