Skip to content

Commit 6952a80

Browse files
osa1Commit Queue
authored andcommitted
[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: #60357 Change-Id: I749001e0e7e9784114415439298c2f3e0fb974b3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/419880 Commit-Queue: Ömer Ağacan <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
1 parent 929657a commit 6952a80

File tree

8 files changed

+210
-10
lines changed

8 files changed

+210
-10
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Benchmarks passing small and large integers to JS via `js_interop`.
6+
//
7+
// In Wasm, integers that fit into 31 bits can be passed without allocation by
8+
// by passing them as `i31ref`. To take advantage of this, dart2wasm checks the
9+
// size of the integer before passing to JS and passes the integer as `i31ref`
10+
// when possible.
11+
//
12+
// This benchmark compares performance of `int` passing for integers that fit
13+
// into 31 bits and those that don't.
14+
15+
import 'dart:js_interop';
16+
17+
import 'package:benchmark_harness/benchmark_harness.dart';
18+
19+
@JS()
20+
external void eval(String code);
21+
22+
// This returns `void` to avoid adding `dartify` overheads to the benchmark
23+
// results.
24+
// V8 can't figure out this doesn't do anything so the loop and JS calls aren't
25+
// eliminated.
26+
@JS()
27+
external void intId(int i);
28+
29+
// Run benchmarked code for at least 2 seconds.
30+
const int minimumMeasureDurationMillis = 2000;
31+
32+
class IntPassingBenchmark {
33+
final int start;
34+
final int end;
35+
36+
IntPassingBenchmark(this.start, this.end);
37+
38+
double measure() =>
39+
BenchmarkBase.measureFor(() {
40+
for (int i = start; i < end; i += 1) {
41+
intId(i);
42+
}
43+
}, minimumMeasureDurationMillis) /
44+
(end - start);
45+
}
46+
47+
void main() {
48+
eval('''
49+
self.intId = (i) => i;
50+
''');
51+
52+
final maxI31 = (1 << 30) - 1;
53+
54+
final small = IntPassingBenchmark(maxI31 - 1000000, maxI31).measure();
55+
report('WasmJSInterop.call.void.1ArgsSmi', small);
56+
57+
final large = IntPassingBenchmark(maxI31 + 1, maxI31 + 1000001).measure();
58+
report('WasmJSInterop.call.void.1ArgsInt', large);
59+
}
60+
61+
/// Reports in Golem-specific format.
62+
void report(String name, double nsPerCall) {
63+
print('$name(RunTimeRaw): $nsPerCall ns.');
64+
}

pkg/dart2wasm/lib/dynamic_modules.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,7 @@ class ConstantCanonicalizer extends ConstantVisitor<void> {
904904
translator.wasmI8Class,
905905
translator.wasmAnyRefClass,
906906
translator.wasmExternRefClass,
907+
translator.wasmI31RefClass,
907908
translator.wasmFuncRefClass,
908909
translator.wasmEqRefClass,
909910
translator.wasmStructRefClass,

pkg/dart2wasm/lib/intrinsics.dart

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ enum StaticIntrinsic {
191191
storeFloatUnaligned('dart:ffi', null, '_storeFloatUnaligned'),
192192
storeDouble('dart:ffi', null, '_storeDouble'),
193193
storeDoubleUnaligned('dart:ffi', null, '_storeDoubleUnaligned'),
194-
;
194+
wasmI31RefNew('dart:_wasm', 'WasmI31Ref', 'fromI32'),
195+
wasmI31RefExtensionsExternalize(
196+
'dart:_wasm', null, 'WasmI31RefExtensions|externalize'),
197+
wasmI31RefExtensionsGetS('dart:_wasm', null, 'WasmI31RefExtensions|get_s'),
198+
wasmI31RefExtensionsGetU('dart:_wasm', null, 'WasmI31RefExtensions|get_u');
195199

196200
final String library;
197201
final String? cls;
@@ -408,12 +412,17 @@ class Intrinsifier {
408412
Member target = node.interfaceTarget;
409413
Class cls = target.enclosingClass!;
410414

411-
// WasmAnyRef.isObject
415+
// WasmAnyRef.isObject, WasmAnyRef.isI31
412416
if (cls == translator.wasmAnyRefClass) {
413-
assert(name == "isObject");
414-
codeGen.translateExpression(receiver, w.RefType.any(nullable: false));
415-
b.ref_test(translator.topInfo.nonNullableType);
416-
return w.NumType.i32;
417+
if (name == "isObject") {
418+
codeGen.translateExpression(receiver, w.RefType.any(nullable: false));
419+
b.ref_test(translator.topInfo.nonNullableType);
420+
return w.NumType.i32;
421+
} else if (name == "isI31") {
422+
codeGen.translateExpression(receiver, w.RefType.any(nullable: false));
423+
b.ref_test(w.RefType.i31(nullable: false));
424+
return w.NumType.i32;
425+
}
417426
}
418427

419428
// WasmArrayRef.length
@@ -1499,6 +1508,30 @@ class Intrinsifier {
14991508
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
15001509
b.emitClassIdRangeCheck(ranges);
15011510
return w.NumType.i32;
1511+
1512+
case StaticIntrinsic.wasmI31RefNew:
1513+
Expression value = node.arguments.positional[0];
1514+
codeGen.translateExpression(value, w.NumType.i32);
1515+
b.i31_new();
1516+
return w.RefType.i31(nullable: false);
1517+
1518+
case StaticIntrinsic.wasmI31RefExtensionsExternalize:
1519+
final value = node.arguments.positional.single;
1520+
codeGen.translateExpression(value, w.RefType.i31(nullable: false));
1521+
b.extern_convert_any();
1522+
return w.RefType.extern(nullable: false);
1523+
1524+
case StaticIntrinsic.wasmI31RefExtensionsGetS:
1525+
final value = node.arguments.positional.single;
1526+
codeGen.translateExpression(value, w.RefType.i31(nullable: false));
1527+
b.i31_get_s();
1528+
return w.NumType.i32;
1529+
1530+
case StaticIntrinsic.wasmI31RefExtensionsGetU:
1531+
final value = node.arguments.positional.single;
1532+
codeGen.translateExpression(value, w.RefType.i31(nullable: false));
1533+
b.i31_get_u();
1534+
return w.NumType.i32;
15021535
}
15031536
}
15041537

pkg/dart2wasm/lib/kernel_nodes.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ mixin KernelNodes {
187187
index.getClass("dart:_wasm", "WasmFunction");
188188
late final Class wasmVoidClass = index.getClass("dart:_wasm", "WasmVoid");
189189
late final Class wasmTableClass = index.getClass("dart:_wasm", "WasmTable");
190+
late final Class wasmI31RefClass = index.getClass("dart:_wasm", "WasmI31Ref");
190191
late final Class wasmArrayClass = index.getClass("dart:_wasm", "WasmArray");
191192
late final Class immutableWasmArrayClass =
192193
index.getClass("dart:_wasm", "ImmutableWasmArray");
@@ -212,6 +213,8 @@ mixin KernelNodes {
212213
index.getTopLevelProcedure("dart:_js_helper", "getInternalizedString");
213214
late final Procedure areEqualInJS =
214215
index.getTopLevelProcedure("dart:_js_helper", "areEqualInJS");
216+
late final Procedure toJSNumber =
217+
index.getTopLevelProcedure("dart:_js_helper", "toJSNumber");
215218

216219
// dart:_js_types procedures
217220
late final Procedure jsStringEquals =

pkg/dart2wasm/lib/translator.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ class Translator with KernelNodes {
294294
wasmF64Class: w.NumType.f64,
295295
wasmAnyRefClass: const w.RefType.any(nullable: false),
296296
wasmExternRefClass: const w.RefType.extern(nullable: false),
297+
wasmI31RefClass: const w.RefType.i31(nullable: false),
297298
wasmFuncRefClass: const w.RefType.func(nullable: false),
298299
wasmEqRefClass: const w.RefType.eq(nullable: false),
299300
wasmStructRefClass: const w.RefType.struct(nullable: false),

sdk/lib/_internal/wasm/lib/js_helper.dart

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ bool areEqualInJS(WasmExternRef? l, WasmExternRef? r) =>
163163
// trip.
164164
double toDartNumber(WasmExternRef? o) => JS<double>("o => o", o);
165165

166+
@pragma('wasm:entry-point')
166167
WasmExternRef? toJSNumber(double o) => JS<WasmExternRef?>("o => o", o);
167168

168169
bool toDartBool(WasmExternRef? o) => JS<bool>("o => o", o);
@@ -331,11 +332,20 @@ WasmExternRef? jsifyRaw(Object? o) {
331332
}
332333
}
333334

334-
@pragma('wasm:prefer-inline')
335-
WasmExternRef? jsifyInt(int o) => toJSNumber(o.toDouble());
335+
WasmExternRef? jsifyInt(int i) {
336+
const int minI31 = -(1 << 30);
337+
const int maxI31 = (1 << 30) - 1;
336338

337-
@pragma('wasm:prefer-inline')
338-
WasmExternRef? jsifyNum(num o) => toJSNumber(o.toDouble());
339+
// Pass small ints as `i31ref` to avoid allocation.
340+
if (i >= minI31 && i <= maxI31) {
341+
return WasmI31Ref.fromI32(WasmI32.fromInt(i)).externalize();
342+
}
343+
344+
return toJSNumber(i.toDouble());
345+
}
346+
347+
WasmExternRef? jsifyNum(num o) =>
348+
o is int ? jsifyInt(o) : toJSNumber(unsafeCast<double>(o));
339349

340350
@pragma('wasm:prefer-inline')
341351
WasmExternRef? jsifyJSValue(JSValue o) => o.toExternRef;

sdk/lib/_wasm/wasm_types.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class WasmAnyRef extends _WasmBase {
3535
/// Whether this reference is a Dart object.
3636
external bool get isObject;
3737

38+
/// Whether this reference is an `i31`.
39+
external bool get isI31;
40+
3841
/// Downcast `anyref` to a Dart object.
3942
///
4043
/// Will throw if the reference is not a Dart object.
@@ -78,6 +81,28 @@ external WasmAnyRef? _internalizeNullable(WasmExternRef? ref);
7881
@pragma("wasm:intrinsic")
7982
external bool _wasmExternRefIsNull(WasmExternRef? ref);
8083

84+
/// The Wasm `i31ref` type.
85+
@pragma("wasm:entry-point")
86+
class WasmI31Ref extends _WasmBase {
87+
/// Wasm `i31.new` instruction.
88+
@pragma("wasm:intrinsic")
89+
external factory WasmI31Ref.fromI32(WasmI32 i);
90+
}
91+
92+
extension WasmI31RefExtensions on WasmI31Ref {
93+
/// Convert a `i31ref` to `externref` with `extern.convert_any`.
94+
@pragma("wasm:intrinsic")
95+
external WasmExternRef? externalize();
96+
97+
/// Wasm `i32.get_s` instruction.
98+
@pragma("wasm:intrinsic")
99+
external WasmI32 get_s();
100+
101+
/// Wasm `i32.get_u` instruction.
102+
@pragma("wasm:intrinsic")
103+
external WasmI32 get_u();
104+
}
105+
81106
/// The Wasm `funcref` type.
82107
@pragma("wasm:entry-point")
83108
class WasmFuncRef extends _WasmBase {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Test dart2wasm's `int` passing to `js_interop` functions.
6+
//
7+
// To avoid allocations when passing an `int` to JS in V8, dart2wasm passes
8+
// `int`s as externalized `i31ref`s. Test the `i31ref` edge cases:
9+
//
10+
// - Min i31 (should be passed as externalized `i31ref`)
11+
// - Max i31 (should be passed as externalized `i31ref`)
12+
// - Min i31 - 1 (should be passed as externalized `f64`)
13+
// - Max i31 + 1 (should be passed as externalized `f64`)
14+
15+
// The option below allows importing `dart:_wasm`.
16+
// dart2wasmOptions=--extra-compiler-option=--enable-experimental-wasm-interop
17+
18+
import 'dart:_wasm';
19+
import 'dart:js_interop';
20+
21+
import 'package:expect/expect.dart';
22+
23+
@JS('test')
24+
external int intTest(int i);
25+
26+
@JS('test')
27+
external num numTest(num i);
28+
29+
void main() {
30+
const int maxI31 = (1 << 30) - 1;
31+
const int minI31 = -(1 << 30);
32+
33+
int i31refs = 0;
34+
int others = 0;
35+
36+
setReturnIdentity =
37+
((JSAny js) {
38+
final isI31Ref = externRefForJSAny(js).internalize()!.isI31;
39+
40+
if (isI31Ref) {
41+
i31refs += 1;
42+
} else {
43+
others += 1;
44+
}
45+
46+
final dartValue = (js.dartify() as double).toInt();
47+
Expect.equals(isI31Ref, dartValue >= minI31 && dartValue <= maxI31);
48+
return js;
49+
}).toJS;
50+
51+
for (int i in <int>[maxI31, maxI31 + 1, minI31, minI31 - 1]) {
52+
returnIdentity(i);
53+
}
54+
55+
Expect.equals(2, i31refs);
56+
Expect.equals(2, others);
57+
}
58+
59+
@JS('globalThis.returnIdentity')
60+
external void set setReturnIdentity(JSFunction fun);
61+
62+
@JS('globalThis.returnIdentity')
63+
external JSAny returnIdentity(int i);

0 commit comments

Comments
 (0)