[dart2wasm] JS interop: pass small ints as i31ref
In V8, the only way to pass a Wasm integer or float to JS without
allocation is by passing it as a 31-bit integer.
This can be done by:
1. Passing as `i32`. If the integer fits into 31 bits it's passed
without allocation.
2. Passing as externalized `i31ref`.
(1) requires importing the JS function with different signatures: for
each `int` argument we would need a signature with the `i32` as the Wasm
argument type, and another with `externref` (or `f64` if we want to pass
large integers as `f64`).
This is not feasible as with a JS function with N `int` arguments we
would need `2^N` imports. So we implement (2): we import each interop
function with one signature, passing `externref` as the argument, as
before. When the number fits into 31 bits we convert it to an `i31ref`
and externalize it. Otherwise we convert the number to `externref` as
before, by calling the JS function `(o) => o` imported with type `[f64]
-> [externref]`.
New benchmark checks `int` passing for small (31 bit) and large (larger
than 31 bit) integers. Results before:
WasmJSInterop.call.void.1ArgsSmi(RunTimeRaw): 0.020 ns.
WasmJSInterop.call.void.1ArgsInt(RunTimeRaw): 0.018 ns.
After:
WasmJSInterop.call.void.1ArgsSmi(RunTimeRaw): 0.014 ns.
WasmJSInterop.call.void.1ArgsInt(RunTimeRaw): 0.018 ns.
Issue: https://github.com/dart-lang/sdk/issues/60357
Change-Id: I749001e0e7e9784114415439298c2f3e0fb974b3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/419880
Commit-Queue: Ömer Ağacan <omersa@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
committed by
Commit Queue
parent
929657adc6
commit
6952a80978
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// Benchmarks passing small and large integers to JS via `js_interop`.
|
||||
//
|
||||
// In Wasm, integers that fit into 31 bits can be passed without allocation by
|
||||
// by passing them as `i31ref`. To take advantage of this, dart2wasm checks the
|
||||
// size of the integer before passing to JS and passes the integer as `i31ref`
|
||||
// when possible.
|
||||
//
|
||||
// This benchmark compares performance of `int` passing for integers that fit
|
||||
// into 31 bits and those that don't.
|
||||
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:benchmark_harness/benchmark_harness.dart';
|
||||
|
||||
@JS()
|
||||
external void eval(String code);
|
||||
|
||||
// This returns `void` to avoid adding `dartify` overheads to the benchmark
|
||||
// results.
|
||||
// V8 can't figure out this doesn't do anything so the loop and JS calls aren't
|
||||
// eliminated.
|
||||
@JS()
|
||||
external void intId(int i);
|
||||
|
||||
// Run benchmarked code for at least 2 seconds.
|
||||
const int minimumMeasureDurationMillis = 2000;
|
||||
|
||||
class IntPassingBenchmark {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
IntPassingBenchmark(this.start, this.end);
|
||||
|
||||
double measure() =>
|
||||
BenchmarkBase.measureFor(() {
|
||||
for (int i = start; i < end; i += 1) {
|
||||
intId(i);
|
||||
}
|
||||
}, minimumMeasureDurationMillis) /
|
||||
(end - start);
|
||||
}
|
||||
|
||||
void main() {
|
||||
eval('''
|
||||
self.intId = (i) => i;
|
||||
''');
|
||||
|
||||
final maxI31 = (1 << 30) - 1;
|
||||
|
||||
final small = IntPassingBenchmark(maxI31 - 1000000, maxI31).measure();
|
||||
report('WasmJSInterop.call.void.1ArgsSmi', small);
|
||||
|
||||
final large = IntPassingBenchmark(maxI31 + 1, maxI31 + 1000001).measure();
|
||||
report('WasmJSInterop.call.void.1ArgsInt', large);
|
||||
}
|
||||
|
||||
/// Reports in Golem-specific format.
|
||||
void report(String name, double nsPerCall) {
|
||||
print('$name(RunTimeRaw): $nsPerCall ns.');
|
||||
}
|
||||
Reference in New Issue
Block a user