[dart2wasm] Take advantage of fast js-string builtins

Some wasm engines have started to optimize the `js-string` builtin
proposal (e.g. V8) and those that haven't yet are probaly going to
do soon.

So we can start taking advantage of it in dart2wasm.

=> We make use of them in the JS<->Dart string copy code.

=> This will also provide a better baseline when evaluating whether
   switching to JS stringes entirely makes sense.

A somewhat unrelated (but necessary for this CL) change is to tighten
the types we use in `@pragma('wasm:import')` and
`@pragma('wasm:export')` in some cases:

We should only use 'pure' wasm types (i.e. not wasm struct / function
types we define for dart classes & functions) and mostly non-composed
types in import/exports as the `--closed-world` optimizations from
binaryen rely on that (and error otherwise).

Overall this leads to significant improvements in Dart<->JS
string copies.

The `WasmDataTransfer.*{From,To}BrowserString` benchmarks
improve something between 50-100%.

Change-Id: I2048113c462ecb2047402c0616d2b3b1f45773f5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/400641
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
This commit is contained in:
Martin Kustermann
2024-12-16 00:55:26 -08:00
committed by Commit Queue
parent e8ebee8845
commit 9b11bf4da9
8 changed files with 142 additions and 328 deletions
+15 -1
View File
@@ -88,7 +88,7 @@ class InlineExpander {
String parameterString = 'x$j';
DartType type = originalArgument.getStaticType(_staticTypeContext);
dartPositionalParameters.add(VariableDeclaration(parameterString,
type: type, isSynthesized: true));
type: _toExternalType(type), isSynthesized: true));
if (originalArgument is! VariableGet) {
allArgumentsAreGet = false;
}
@@ -121,4 +121,18 @@ class InlineExpander {
_methodCollector.addMethod(dartProcedure, jsMethodName, codeTemplate);
return result;
}
// The `JS<foo>("...some js code ...", arg0, arg1)` expressions will produce
// wasm imports. We want to only use types in those wasm imports that binaryen
// allows under closed world assumptions (and not blindly use `arg<N>`s static
// type).
//
// For now we special case `WasmArray<>` which we turn into a generic `array`
// type (super type of all wasm arrays).
DartType _toExternalType(DartType type) {
if (type is InterfaceType && type.classNode == _util.wasmArrayClass) {
return InterfaceType(_util.wasmArrayRefClass, type.declaredNullability);
}
return type;
}
}
+20
View File
@@ -129,6 +129,26 @@ const jsRuntimeBlobPart3 = r'''
"fromCharCode": (i) => String.fromCharCode(i),
"length": (s) => s.length,
"substring": (s, a, b) => s.substring(a, b),
"fromCharCodeArray": (a, start, end) => {
if (end <= start) return '';
const read = dartInstance.exports.$wasmI16ArrayGet;
let result = '';
let index = start;
const chunkLength = Math.min(end - index, 500);
let array = new Array(chunkLength);
while (index < end) {
const newChunkLength = Math.min(end - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(a, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
},
};
const deferredLibraryHelper = {
+5
View File
@@ -27,6 +27,8 @@ class CoreTypesUtil {
final Procedure jsValueUnboxTarget;
final Procedure numToIntTarget;
final Class wasmExternRefClass;
final Class wasmArrayClass;
final Class wasmArrayRefClass;
final Procedure wrapDartFunctionTarget;
CoreTypesUtil(this.coreTypes, this._extensionIndex)
@@ -64,6 +66,9 @@ class CoreTypesUtil {
.firstWhere((p) => p.name.text == 'unbox'),
wasmExternRefClass =
coreTypes.index.getClass('dart:_wasm', 'WasmExternRef'),
wasmArrayClass = coreTypes.index.getClass('dart:_wasm', 'WasmArray'),
wasmArrayRefClass =
coreTypes.index.getClass('dart:_wasm', 'WasmArrayRef'),
wrapDartFunctionTarget = coreTypes.index
.getTopLevelProcedure('dart:_js_helper', '_wrapDartFunction');
+27 -1
View File
@@ -690,7 +690,7 @@ class Translator with KernelNodes {
/// (`anyref`, `funcref` or `externref`).
/// This function can be called before the class info is built.
w.ValueType translateExternalType(DartType type) {
bool isPotentiallyNullable = type.isPotentiallyNullable;
final bool isPotentiallyNullable = type.isPotentiallyNullable;
if (type is InterfaceType) {
Class cls = type.classNode;
if (cls == wasmFuncRefClass || cls == wasmFunctionClass) {
@@ -699,6 +699,16 @@ class Translator with KernelNodes {
if (cls == wasmExternRefClass) {
return w.RefType.extern(nullable: isPotentiallyNullable);
}
if (cls == wasmArrayRefClass) {
return w.RefType.array(nullable: isPotentiallyNullable);
}
if (cls == wasmArrayClass) {
final elementType =
translateExternalStorageType(type.typeArguments.single);
return w.RefType.def(
wasmArrayType(elementType, '$elementType', mutable: true),
nullable: isPotentiallyNullable);
}
if (!isPotentiallyNullable) {
w.StorageType? builtin = builtinTypes[cls];
if (builtin != null && builtin.isPrimitive) {
@@ -711,6 +721,22 @@ class Translator with KernelNodes {
return w.RefType.any(nullable: true);
}
w.StorageType translateExternalStorageType(DartType type) {
if (type is InterfaceType) {
final cls = type.classNode;
if (isWasmType(cls)) {
final isNullable = type.isPotentiallyNullable;
final w.StorageType? builtin = builtinTypes[cls];
if (builtin != null) {
if (!isNullable) return builtin;
if (builtin.isPrimitive) throw "Wasm numeric types can't be nullable";
return (builtin as w.RefType).withNullability(isNullable);
}
}
}
return translateExternalType(type) as w.RefType;
}
/// Creates a global reference to [f] in [module]. [f] must also be located
/// in [module].
w.Global makeFunctionRef(w.ModuleBuilder module, w.BaseFunction f) {
+35 -325
View File
@@ -15,63 +15,22 @@ import 'dart:typed_data';
@pragma('wasm:prefer-inline')
JSStringImpl jsStringFromDartString(String s) {
if (s is OneByteString) {
final fromArray = s.array;
final toArray = WasmArray<WasmI16>(fromArray.length);
for (int i = 0; i < fromArray.length; ++i) {
toArray.write(i, fromArray.readUnsigned(i));
}
return JSStringImpl(
JS<WasmExternRef>(
r'''
(s, length) => {
if (length == 0) return '';
const read = dartInstance.exports.$stringRead1;
let result = '';
let index = 0;
const chunkLength = Math.min(length - index, 500);
let array = new Array(chunkLength);
while (index < length) {
const newChunkLength = Math.min(length - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(s, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
}
''',
jsObjectFromDartObject(s),
s.length.toWasmI32(),
_jsStringFromCharCodeArray(
toArray,
0.toWasmI32(),
toArray.length.toWasmI32(),
),
);
}
if (s is TwoByteString) {
return JSStringImpl(
JS<WasmExternRef>(
r'''
(s, length) => {
if (length == 0) return '';
const read = dartInstance.exports.$stringRead2;
let result = '';
let index = 0;
const chunkLength = Math.min(length - index, 500);
let array = new Array(chunkLength);
while (index < length) {
const newChunkLength = Math.min(length - index, 500);
for (let i = 0; i < newChunkLength; i++) {
array[i] = read(s, index++);
}
if (newChunkLength < chunkLength) {
array = array.slice(0, newChunkLength);
}
result += String.fromCharCode(...array);
}
return result;
}
''',
jsObjectFromDartObject(s),
s.length.toWasmI32(),
),
_jsStringFromCharCodeArray(s.array, 0.toWasmI32(), s.length.toWasmI32()),
);
}
@@ -79,70 +38,34 @@ JSStringImpl jsStringFromDartString(String s) {
}
@patch
@pragma('wasm:prefer-inline')
String jsStringToDartString(JSStringImpl s) {
final length = s.length;
if (length == 0) return '';
return JS<String>(r'''
(s) => {
let length = s.length;
let range = 0;
for (let i = 0; i < length; i++) {
range |= s.codePointAt(i);
}
const exports = dartInstance.exports;
if (range < 256) {
if (length <= 10) {
if (length == 1) {
return exports.$stringAllocate1_1(s.codePointAt(0));
}
if (length == 2) {
return exports.$stringAllocate1_2(s.codePointAt(0), s.codePointAt(1));
}
if (length == 3) {
return exports.$stringAllocate1_3(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2));
}
if (length == 4) {
return exports.$stringAllocate1_4(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3));
}
if (length == 5) {
return exports.$stringAllocate1_5(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4));
}
if (length == 6) {
return exports.$stringAllocate1_6(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5));
}
if (length == 7) {
return exports.$stringAllocate1_7(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6));
}
if (length == 8) {
return exports.$stringAllocate1_8(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7));
}
if (length == 9) {
return exports.$stringAllocate1_9(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8));
}
if (length == 10) {
return exports.$stringAllocate1_10(s.codePointAt(0), s.codePointAt(1), s.codePointAt(2), s.codePointAt(3), s.codePointAt(4), s.codePointAt(5), s.codePointAt(6), s.codePointAt(7), s.codePointAt(8), s.codePointAt(9));
}
}
const dartString = exports.$stringAllocate1(length);
const write = exports.$stringWrite1;
for (let i = 0; i < length; i++) {
write(dartString, i, s.codePointAt(i));
}
return dartString;
} else {
const dartString = exports.$stringAllocate2(length);
const write = exports.$stringWrite2;
for (let i = 0; i < length; i++) {
write(dartString, i, s.charCodeAt(i));
}
return dartString;
}
String jsStringToDartString(JSStringImpl jsString) {
final length = jsString.length;
two_byte:
{
final oneByteString = OneByteString.withLength(length);
final array = oneByteString.array;
for (int i = 0; i < length; ++i) {
final int codeUnit = jsString.codeUnitAtUnchecked(i);
if (codeUnit > 255) break two_byte;
array.write(i, codeUnit);
}
''', s.toExternRef);
return oneByteString;
}
final twoByteString = TwoByteString.withLength(length);
final array = twoByteString.array;
for (int i = 0; i < length; ++i) {
array.write(i, jsString.codeUnitAtUnchecked(i));
}
return twoByteString;
}
@pragma("wasm:import", "wasm:js-string.fromCharCodeArray")
external WasmExternRef _jsStringFromCharCodeArray(
WasmArray<WasmI16> array,
WasmI32 start,
WasmI32 end,
);
@pragma('wasm:prefer-inline')
void _copyFromWasmI8Array(
WasmExternRef jsArray,
@@ -541,219 +464,6 @@ WasmExternRef jsDataViewFromDartByteData(ByteData l, int length) {
);
}
@pragma("wasm:export", "\$stringAllocate1")
OneByteString _stringAllocate1(WasmI32 length) {
return OneByteString.withLength(length.toIntSigned());
}
@pragma("wasm:export", "\$stringAllocate1_1")
OneByteString _stringAllocate1_1(WasmI32 a0) {
final result = OneByteString.withLength(1);
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_2")
OneByteString _stringAllocate1_2(WasmI32 a0, WasmI32 a1) {
final result = OneByteString.withLength(2);
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_3")
OneByteString _stringAllocate1_3(WasmI32 a0, WasmI32 a1, WasmI32 a2) {
final result = OneByteString.withLength(3);
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_4")
OneByteString _stringAllocate1_4(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
) {
final result = OneByteString.withLength(4);
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_5")
OneByteString _stringAllocate1_5(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
) {
final result = OneByteString.withLength(5);
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_6")
OneByteString _stringAllocate1_6(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
WasmI32 a5,
) {
final result = OneByteString.withLength(6);
result.setUnchecked(5, a5.toIntSigned());
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_7")
OneByteString _stringAllocate1_7(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
WasmI32 a5,
WasmI32 a6,
) {
final result = OneByteString.withLength(7);
result.setUnchecked(6, a6.toIntSigned());
result.setUnchecked(5, a5.toIntSigned());
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_8")
OneByteString _stringAllocate1_8(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
WasmI32 a5,
WasmI32 a6,
WasmI32 a7,
) {
final result = OneByteString.withLength(8);
result.setUnchecked(7, a7.toIntSigned());
result.setUnchecked(6, a6.toIntSigned());
result.setUnchecked(5, a5.toIntSigned());
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_9")
OneByteString _stringAllocate1_9(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
WasmI32 a5,
WasmI32 a6,
WasmI32 a7,
WasmI32 a8,
) {
final result = OneByteString.withLength(9);
result.setUnchecked(8, a8.toIntSigned());
result.setUnchecked(7, a7.toIntSigned());
result.setUnchecked(6, a6.toIntSigned());
result.setUnchecked(5, a5.toIntSigned());
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringAllocate1_10")
OneByteString _stringAllocate1_10(
WasmI32 a0,
WasmI32 a1,
WasmI32 a2,
WasmI32 a3,
WasmI32 a4,
WasmI32 a5,
WasmI32 a6,
WasmI32 a7,
WasmI32 a8,
WasmI32 a9,
) {
final result = OneByteString.withLength(10);
result.setUnchecked(9, a9.toIntSigned());
result.setUnchecked(8, a8.toIntSigned());
result.setUnchecked(7, a7.toIntSigned());
result.setUnchecked(6, a6.toIntSigned());
result.setUnchecked(5, a5.toIntSigned());
result.setUnchecked(4, a4.toIntSigned());
result.setUnchecked(3, a3.toIntSigned());
result.setUnchecked(2, a2.toIntSigned());
result.setUnchecked(1, a1.toIntSigned());
result.setUnchecked(0, a0.toIntSigned());
return result;
}
@pragma("wasm:export", "\$stringRead1")
WasmI32 _stringRead1(WasmExternRef? ref, WasmI32 index) {
final string = unsafeCastOpaque<OneByteString>(
unsafeCast<WasmExternRef>(ref).internalize(),
);
return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32();
}
@pragma("wasm:export", "\$stringWrite1")
void _stringWrite1(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) {
final string = unsafeCastOpaque<OneByteString>(
unsafeCast<WasmExternRef>(ref).internalize(),
);
string.setUnchecked(index.toIntSigned(), codePoint.toIntSigned());
}
@pragma("wasm:export", "\$stringAllocate2")
TwoByteString _stringAllocate2(WasmI32 length) {
return TwoByteString.withLength(length.toIntSigned());
}
@pragma("wasm:export", "\$stringRead2")
WasmI32 _stringRead2(WasmExternRef? ref, WasmI32 index) {
final string = unsafeCastOpaque<TwoByteString>(
unsafeCast<WasmExternRef>(ref).internalize(),
);
return string.codeUnitAtUnchecked(index.toIntSigned()).toWasmI32();
}
@pragma("wasm:export", "\$stringWrite2")
void _stringWrite2(WasmExternRef? ref, WasmI32 index, WasmI32 codePoint) {
final string = unsafeCastOpaque<TwoByteString>(
unsafeCast<WasmExternRef>(ref).internalize(),
);
string.setUnchecked(index.toIntSigned(), codePoint.toIntSigned());
}
@pragma("wasm:export", "\$wasmI8ArrayGet")
WasmI32 _wasmI8ArrayGet(WasmExternRef? ref, WasmI32 index) {
final array = unsafeCastOpaque<WasmArray<WasmI8>>(
@@ -60,7 +60,6 @@ final class JSStringImpl implements String, StringUncheckedOperationsBase {
final s = o.toString();
final jsString =
s is JSStringImpl ? js.JSValue.boxT<JSAny?>(s.toExternRef) : s.toJS;
// array._setUnchecked(i, jsString);
array[i] = jsString;
}
return JSStringImpl(
+6
View File
@@ -31,6 +31,9 @@ extension OneByteStringUncheckedOperations on OneByteString {
@pragma('wasm:prefer-inline')
void setUnchecked(int index, int codePoint) => _setAt(index, codePoint);
@pragma('wasm:prefer-inline')
WasmArray<WasmI8> get array => _array;
}
extension TwoByteStringUncheckedOperations on TwoByteString {
@@ -43,6 +46,9 @@ extension TwoByteStringUncheckedOperations on TwoByteString {
@pragma('wasm:prefer-inline')
void setUnchecked(int index, int codePoint) => _setAt(index, codePoint);
@pragma('wasm:prefer-inline')
WasmArray<WasmI16> get array => _array;
}
/// Static function for `OneByteString._array` to avoid making `_array` public.
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2024, 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.
import 'dart:js_interop';
import 'package:expect/expect.dart';
main() async {
final String oneByteString = makeLongString('012345789');
final String twoByteString = makeLongString('01234中6789');
Expect.equals(oneByteString, roundTrip(oneByteString));
Expect.equals(twoByteString, roundTrip(twoByteString));
}
/// Ensure we make a very long string, ensuring that we'll also hit slow paths
/// in the string copy implementation.
String makeLongString(String string) {
while (string.length < 1024 * 1024) {
string = string + string;
}
return string;
}
/// Copies the string to JS and back again to a dart internal String.
String roundTrip(String dartString) {
final JSString jsString = dartString.toJS;
// Using string interpolation will force conversion to internal string (vs
// `JSStringImpl`)
final string = 'A${jsString.toDart}Z';
return string.substring(1, string.length - 1);
}