Skip to content

Commit db44830

Browse files
osa1Commit Queue
authored and
Commit Queue
committed
[dart2wasm] Pass Dart double as f64 to JS interop
Don't convert Dart `double`s to `externref`s when calling JS interop functions, pass them as `double`. We pass other unboxed values (`int`s and `bool`s) as `externref`s, as before: - `int`s are most efficiently passed as `externref`s, as small integers can be converted to `i31ref` and externalized without allocation. - `bool`s passed as `i32` mostly work because JS treats 0 as false and everything else as true, but the values can still be observed as numbers rather than bools, which causes some test failures. Changes: - Make raw interop procedures take `double` as argument, when the Dart type for the interop function argument is non-nullable `double`. - Pass static type of the value and expected type (by the interop function) to `jsifyValue`. - `jsifyValue` then takes the static type and expected type into account to avoid conversions when both are `double`s. - `jsifyValue` is refactored to avoid the type conversion mapping allocation on every call. New benchmark result before the changes: WasmJSInterop.call.void.1ArgsDouble(RunTimeRaw): 0.018275229357798167 ns. After: WasmJSInterop.call.void.1ArgsDouble(RunTimeRaw): 0.014034965034965034 ns. Issue: #60357 Change-Id: Ia70671f9a8e14f359f1119beda123e94aacdd2cd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/422480 Commit-Queue: Ömer Ağacan <[email protected]> Reviewed-by: Srujan Gaddam <[email protected]>
1 parent 55dc02c commit db44830

File tree

4 files changed

+166
-93
lines changed

4 files changed

+166
-93
lines changed

benchmarks/WasmJSInterop/WasmJSInterop.dart

+34-6
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import 'package:benchmark_harness/benchmark_harness.dart';
1919
@JS()
2020
external void eval(String code);
2121

22-
// This returns `void` to avoid adding `dartify` overheads to the benchmark
22+
// These return `void` to avoid adding `dartify` overheads to the benchmark
2323
// results.
24-
// V8 can't figure out this doesn't do anything so the loop and JS calls aren't
25-
// eliminated.
24+
// V8 can't figure out that these don't do anything so the loops and JS calls
25+
// aren't eliminated.
2626
@JS()
27-
external void intId(int i);
27+
external void intFun(int i);
28+
29+
@JS()
30+
external void doubleFun(double d);
2831

2932
// Run benchmarked code for at least 2 seconds.
3033
const int minimumMeasureDurationMillis = 2000;
@@ -38,15 +41,34 @@ class IntPassingBenchmark {
3841
double measure() =>
3942
BenchmarkBase.measureFor(() {
4043
for (int i = start; i < end; i += 1) {
41-
intId(i);
44+
intFun(i);
4245
}
4346
}, minimumMeasureDurationMillis) /
4447
(end - start);
4548
}
4649

50+
class DoublePassingBenchmark {
51+
final double start;
52+
final double step;
53+
final int calls;
54+
55+
DoublePassingBenchmark(this.start, this.step, this.calls);
56+
57+
double measure() =>
58+
BenchmarkBase.measureFor(() {
59+
double d = start;
60+
for (int i = 0; i < calls; i += 1) {
61+
doubleFun(d);
62+
d *= step;
63+
}
64+
}, minimumMeasureDurationMillis) /
65+
calls;
66+
}
67+
4768
void main() {
4869
eval('''
49-
self.intId = (i) => i;
70+
self.intFun = (i) => i;
71+
self.doubleFun = (d) => d;
5072
''');
5173

5274
final maxI31 = (1 << 30) - 1;
@@ -56,6 +78,12 @@ void main() {
5678

5779
final large = IntPassingBenchmark(maxI31 + 1, maxI31 + 1000001).measure();
5880
report('WasmJSInterop.call.void.1ArgsInt', large);
81+
82+
// Have more than one call site to the `double` benchmark to avoid inlining
83+
// too much, and for fair comparison with the `int` benchmark above.
84+
DoublePassingBenchmark(1.0, 1.0, 10).measure();
85+
final double = DoublePassingBenchmark(1.0, 12.34, 1000000).measure();
86+
report('WasmJSInterop.call.void.1ArgsDouble', double);
5987
}
6088

6189
/// Reports in Golem-specific format.

pkg/dart2wasm/lib/js/callback_specializer.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class CallbackSpecializer {
5353
type: callExpr.getStaticType(_staticTypeContext),
5454
isSynthesized: true);
5555

56-
final jsified = jsifyValue(temp, _util, _staticTypeContext.typeEnvironment);
56+
final jsified = jsifyValue(temp, _util.nullableWasmExternRefType, _util,
57+
_staticTypeContext.typeEnvironment);
5758

5859
return ReturnStatement(Let(temp, jsified));
5960
}

pkg/dart2wasm/lib/js/interop_specializer.dart

+50-29
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,16 @@ abstract class _Specializer {
8080
// Initialize variable declarations.
8181
List<String> jsParameterStrings = [];
8282
List<VariableDeclaration> dartPositionalParameters = [];
83-
for (int j = 0; j < parameters.length; j++) {
84-
String parameterString = 'x$j';
83+
for (int i = 0; i < parameters.length; i++) {
84+
final VariableDeclaration parameter = parameters[i];
85+
final DartType parameterType = parameter.type;
86+
final interopFunctionParameterType =
87+
parameterType == _util.coreTypes.doubleNonNullableRawType
88+
? _util.coreTypes.doubleNonNullableRawType
89+
: _util.nullableWasmExternRefType;
90+
String parameterString = 'x$i';
8591
dartPositionalParameters.add(VariableDeclaration(parameterString,
86-
type: _util.nullableWasmExternRefType, isSynthesized: true));
92+
type: interopFunctionParameterType, isSynthesized: true));
8793
jsParameterStrings.add(parameterString);
8894
}
8995

@@ -130,12 +136,19 @@ abstract class _ProcedureSpecializer extends _Specializer {
130136
@override
131137
Expression specialize() {
132138
// Return the replacement body.
133-
Expression invocation = StaticInvocation(
134-
_getOrCreateInteropProcedure(),
135-
Arguments(parameters
136-
.map<Expression>((variable) => jsifyValue(variable, factory._util,
137-
factory._staticTypeContext.typeEnvironment))
138-
.toList()));
139+
final interopProcedure = _getOrCreateInteropProcedure();
140+
final interopProcedureType =
141+
interopProcedure.computeSignatureOrFunctionType();
142+
final List<Expression> jsifiedArguments = [];
143+
for (int i = 0; i < parameters.length; i += 1) {
144+
jsifiedArguments.add(jsifyValue(
145+
parameters[i],
146+
interopProcedureType.positionalParameters[i],
147+
factory._util,
148+
factory._staticTypeContext.typeEnvironment));
149+
}
150+
final invocation =
151+
StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
139152
return _util.castInvocationForReturn(invocation, function.returnType);
140153
}
141154
}
@@ -225,18 +238,23 @@ abstract class _PositionalInvocationSpecializer extends _InvocationSpecializer {
225238
Expression specialize() {
226239
// Create or get the specialized procedure for the invoked number of
227240
// arguments. Cast as needed and return the final invocation.
228-
final staticInvocation = StaticInvocation(
229-
_getOrCreateInteropProcedure(),
230-
Arguments(invocation.arguments.positional.map<Expression>((expr) {
231-
final temp = VariableDeclaration(null,
232-
initializer: expr,
233-
type: expr.getStaticType(factory._staticTypeContext),
234-
isSynthesized: true);
235-
return Let(
236-
temp,
237-
jsifyValue(temp, factory._util,
238-
factory._staticTypeContext.typeEnvironment));
239-
}).toList()));
241+
final interopProcedure = _getOrCreateInteropProcedure();
242+
final interopProcedureType =
243+
interopProcedure.computeSignatureOrFunctionType();
244+
final List<Expression> jsifiedArguments = [];
245+
final List<Expression> arguments = invocation.arguments.positional;
246+
for (int i = 0; i < arguments.length; i += 1) {
247+
final temp = VariableDeclaration(null,
248+
initializer: arguments[i],
249+
type: arguments[i].getStaticType(factory._staticTypeContext),
250+
isSynthesized: true);
251+
jsifiedArguments.add(Let(
252+
temp,
253+
jsifyValue(temp, interopProcedureType.positionalParameters[i],
254+
factory._util, factory._staticTypeContext.typeEnvironment)));
255+
}
256+
final staticInvocation =
257+
StaticInvocation(interopProcedure, Arguments(jsifiedArguments));
240258
return _util.castInvocationForReturn(
241259
staticInvocation, invocation.getStaticType(_staticTypeContext));
242260
}
@@ -321,21 +339,24 @@ class _ObjectLiteralSpecializer extends _InvocationSpecializer {
321339
for (NamedExpression expr in invocation.arguments.named) {
322340
namedArgs[expr.name] = expr.value;
323341
}
342+
final interopProcedureType =
343+
interopProcedure.computeSignatureOrFunctionType();
324344
final arguments =
325345
parameters.map<Expression>((decl) => namedArgs[decl.name!]!).toList();
326-
final positionalArgs = arguments.map<Expression>((expr) {
346+
final List<Expression> jsifiedArguments = [];
347+
for (int i = 0; i < arguments.length; i += 1) {
327348
final temp = VariableDeclaration(null,
328-
initializer: expr,
329-
type: expr.getStaticType(_staticTypeContext),
349+
initializer: arguments[i],
350+
type: arguments[i].getStaticType(factory._staticTypeContext),
330351
isSynthesized: true);
331-
return Let(
352+
jsifiedArguments.add(Let(
332353
temp,
333-
jsifyValue(
334-
temp, factory._util, factory._staticTypeContext.typeEnvironment));
335-
}).toList();
354+
jsifyValue(temp, interopProcedureType.positionalParameters[i],
355+
factory._util, factory._staticTypeContext.typeEnvironment)));
356+
}
336357
assert(factory._extensionIndex.isStaticInteropType(function.returnType));
337358
return invokeOneArg(_util.jsValueBoxTarget,
338-
StaticInvocation(interopProcedure, Arguments(positionalArgs)));
359+
StaticInvocation(interopProcedure, Arguments(jsifiedArguments)));
339360
}
340361
}
341362

pkg/dart2wasm/lib/js/util.dart

+80-57
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,41 @@ class CoreTypesUtil {
9292
final Class jsArrayBufferImplClass;
9393
final Class byteBufferClass;
9494

95+
// NB. We rely on iteration ordering being insertion order to handle subtypes
96+
// before supertypes to convert as `int` and `double` before `num`.
97+
late final Map<Class, Procedure> _externRefConverterMap = {
98+
coreTypes.boolClass: toJSBoolean,
99+
coreTypes.intClass: jsifyInt,
100+
coreTypes.doubleClass: toJSNumber,
101+
coreTypes.numClass: jsifyNum,
102+
jsValueClass: jsifyJSValue,
103+
coreTypes.stringClass: jsifyString,
104+
jsInt8ArrayImplClass: jsifyJSInt8ArrayImpl,
105+
jsUint8ArrayImplClass: jsifyJSUint8ArrayImpl,
106+
jsUint8ClampedArrayImplClass: jsifyJSUint8ClampedArrayImpl,
107+
jsInt16ArrayImplClass: jsifyJSInt16ArrayImpl,
108+
jsUint16ArrayImplClass: jsifyJSUint16ArrayImpl,
109+
jsInt32ArrayImplClass: jsifyJSInt32ArrayImpl,
110+
jsUint32ArrayImplClass: jsifyJSUint32ArrayImpl,
111+
jsFloat32ArrayImplClass: jsifyJSFloat32ArrayImpl,
112+
jsFloat64ArrayImplClass: jsifyJSFloat64ArrayImpl,
113+
int8ListClass: jsInt8ArrayFromDartInt8List,
114+
uint8ListClass: jsUint8ArrayFromDartUint8List,
115+
uint8ClampedListClass: jsUint8ClampedArrayFromDartUint8ClampedList,
116+
int16ListClass: jsInt16ArrayFromDartInt16List,
117+
uint16ListClass: jsUint16ArrayFromDartUint16List,
118+
int32ListClass: jsInt32ArrayFromDartInt32List,
119+
uint32ListClass: jsUint32ArrayFromDartUint32List,
120+
float32ListClass: jsFloat32ArrayFromDartFloat32List,
121+
float64ListClass: jsFloat64ArrayFromDartFloat64List,
122+
jsDataViewImplClass: jsifyJSDataViewImpl,
123+
byteDataClass: jsifyByteData,
124+
coreTypes.listClass: jsifyRawList,
125+
jsArrayBufferImplClass: jsifyJSArrayBufferImpl,
126+
byteBufferClass: jsArrayBufferFromDartByteBuffer,
127+
coreTypes.functionClass: jsifyFunction,
128+
};
129+
95130
CoreTypesUtil(this.coreTypes, this.extensionIndex)
96131
: allowInteropTarget = coreTypes.index
97132
.getTopLevelProcedure('dart:js_util', 'allowInterop'),
@@ -367,6 +402,43 @@ class CoreTypesUtil {
367402
}
368403
return AsExpression(expression, staticType);
369404
}
405+
406+
/// Return the function to convert a value with type [valueType] to Dart
407+
/// interop type [expectedType].
408+
///
409+
/// [expectedType] can be any interop type, but for now this only handles the
410+
/// interop types generated by [_Specializer._getRawInteropProcedure].
411+
///
412+
/// `null` return value means no conversion is needed, the value can be passed
413+
/// to the interop function directly.
414+
///
415+
/// The argument passed to the returned conversion function needs to be
416+
/// non-nullable. This function does not check the nullability of [valueType]
417+
/// and assume that the argument passed to the conversion function won't be
418+
/// `null`.
419+
Procedure? _conversionProcedure(
420+
DartType valueType, DartType expectedType, TypeEnvironment typeEnv) {
421+
if (expectedType == coreTypes.doubleNonNullableRawType) {
422+
assert(valueType is InterfaceType &&
423+
valueType.classNode == coreTypes.doubleClass);
424+
return null;
425+
}
426+
427+
assert(expectedType == nullableWasmExternRefType,
428+
'Unexpected expected type: $expectedType');
429+
430+
for (final entry in _externRefConverterMap.entries) {
431+
if (typeEnv.isSubtypeOf(
432+
valueType,
433+
InterfaceType(entry.key, Nullability.nonNullable),
434+
SubtypeCheckMode.withNullabilities)) {
435+
return entry.value;
436+
}
437+
}
438+
439+
// `dynamic` or `Object?`, convert based on runtime type.
440+
return jsifyRawTarget;
441+
}
370442
}
371443

372444
StaticInvocation invokeOneArg(Procedure target, Expression arg) =>
@@ -383,20 +455,22 @@ InstanceInvocation invokeMethod(VariableDeclaration receiver, Procedure target,
383455
bool parametersNeedParens(List<String> parameters) =>
384456
parameters.isEmpty || parameters.length > 1;
385457

386-
Expression jsifyValue(VariableDeclaration variable, CoreTypesUtil coreTypes,
387-
TypeEnvironment typeEnv) {
388-
final Procedure conversionProcedure;
458+
Expression jsifyValue(VariableDeclaration variable, DartType expectedType,
459+
CoreTypesUtil coreTypes, TypeEnvironment typeEnv) {
460+
final Procedure? conversionProcedure;
389461

390462
if (coreTypes.extensionIndex.isStaticInteropType(variable.type) ||
391463
coreTypes.extensionIndex.isExternalDartReferenceType(variable.type)) {
392464
conversionProcedure = coreTypes.jsValueUnboxTarget;
393465
} else {
394466
conversionProcedure =
395-
_conversionProcedure(variable.type, coreTypes, typeEnv);
467+
coreTypes._conversionProcedure(variable.type, expectedType, typeEnv);
396468
}
397469

398-
final conversion =
399-
StaticInvocation(conversionProcedure, Arguments([VariableGet(variable)]));
470+
final conversion = conversionProcedure == null
471+
? VariableGet(variable)
472+
: StaticInvocation(
473+
conversionProcedure, Arguments([VariableGet(variable)]));
400474

401475
if (variable.type.isPotentiallyNullable) {
402476
return ConditionalExpression(
@@ -408,54 +482,3 @@ Expression jsifyValue(VariableDeclaration variable, CoreTypesUtil coreTypes,
408482
return conversion;
409483
}
410484
}
411-
412-
Procedure _conversionProcedure(
413-
DartType type, CoreTypesUtil util, TypeEnvironment typeEnv) {
414-
// NB. We rely on iteration ordering being insertion order to handle subtypes
415-
// before supertypes to convert as `int` and `double` before `num`.
416-
final Map<Class, Procedure> converterMap = {
417-
util.coreTypes.boolClass: util.toJSBoolean,
418-
util.coreTypes.intClass: util.jsifyInt,
419-
util.coreTypes.doubleClass: util.toJSNumber,
420-
util.coreTypes.numClass: util.jsifyNum,
421-
util.jsValueClass: util.jsifyJSValue,
422-
util.coreTypes.stringClass: util.jsifyString,
423-
util.jsInt8ArrayImplClass: util.jsifyJSInt8ArrayImpl,
424-
util.jsUint8ArrayImplClass: util.jsifyJSUint8ArrayImpl,
425-
util.jsUint8ClampedArrayImplClass: util.jsifyJSUint8ClampedArrayImpl,
426-
util.jsInt16ArrayImplClass: util.jsifyJSInt16ArrayImpl,
427-
util.jsUint16ArrayImplClass: util.jsifyJSUint16ArrayImpl,
428-
util.jsInt32ArrayImplClass: util.jsifyJSInt32ArrayImpl,
429-
util.jsUint32ArrayImplClass: util.jsifyJSUint32ArrayImpl,
430-
util.jsFloat32ArrayImplClass: util.jsifyJSFloat32ArrayImpl,
431-
util.jsFloat64ArrayImplClass: util.jsifyJSFloat64ArrayImpl,
432-
util.int8ListClass: util.jsInt8ArrayFromDartInt8List,
433-
util.uint8ListClass: util.jsUint8ArrayFromDartUint8List,
434-
util.uint8ClampedListClass:
435-
util.jsUint8ClampedArrayFromDartUint8ClampedList,
436-
util.int16ListClass: util.jsInt16ArrayFromDartInt16List,
437-
util.uint16ListClass: util.jsUint16ArrayFromDartUint16List,
438-
util.int32ListClass: util.jsInt32ArrayFromDartInt32List,
439-
util.uint32ListClass: util.jsUint32ArrayFromDartUint32List,
440-
util.float32ListClass: util.jsFloat32ArrayFromDartFloat32List,
441-
util.float64ListClass: util.jsFloat64ArrayFromDartFloat64List,
442-
util.jsDataViewImplClass: util.jsifyJSDataViewImpl,
443-
util.byteDataClass: util.jsifyByteData,
444-
util.coreTypes.listClass: util.jsifyRawList,
445-
util.jsArrayBufferImplClass: util.jsifyJSArrayBufferImpl,
446-
util.byteBufferClass: util.jsArrayBufferFromDartByteBuffer,
447-
util.coreTypes.functionClass: util.jsifyFunction,
448-
};
449-
450-
for (final entry in converterMap.entries) {
451-
if (typeEnv.isSubtypeOf(
452-
type,
453-
InterfaceType(entry.key, Nullability.nonNullable),
454-
SubtypeCheckMode.withNullabilities)) {
455-
return entry.value;
456-
}
457-
}
458-
459-
// `dynamic` or `Object?`, convert based on runtime type.
460-
return util.jsifyRawTarget;
461-
}

0 commit comments

Comments
 (0)