Skip to content

Commit ab09f99

Browse files
mkustermannCommit Queue
authored and
Commit Queue
committed
[dart2wasm] Outline <obj> is/as <type> checks to shared helper functions
This reduces in Flute size of optimized wasm by * 1.5% in -O3 * 0.7% in -O4 We rely on binaryen to inline small enough checks (e.g. `is` check with a single cid-range) Change-Id: I8f2c093daebe47d6e066b8681c1e285fa5fa0ea0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/351780 Reviewed-by: Slava Egorov <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent c906c3a commit ab09f99

File tree

5 files changed

+168
-69
lines changed

5 files changed

+168
-69
lines changed

pkg/dart2wasm/lib/async.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ class AsyncCodeGenerator extends CodeGenerator {
10691069
if (emitGuard) {
10701070
_getCurrentException();
10711071
b.ref_as_non_null();
1072-
types.emitTypeCheck(this, catch_.guard,
1072+
types.emitIsTest(this, catch_.guard,
10731073
translator.coreTypes.objectNonNullableRawType, catch_);
10741074
b.i32_eqz();
10751075
// When generating guards we can't generate the catch body inside the

pkg/dart2wasm/lib/code_generator.dart

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,7 +1158,7 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
11581158
// Only emit the type test if the guard is not [Object].
11591159
if (emitGuard) {
11601160
b.local_get(thrownException);
1161-
types.emitTypeCheck(
1161+
types.emitIsTest(
11621162
this, guard, translator.coreTypes.objectNonNullableRawType, catch_);
11631163
b.i32_eqz();
11641164
b.br_if(catchBlock);
@@ -3025,8 +3025,12 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
30253025

30263026
@override
30273027
w.ValueType visitIsExpression(IsExpression node, w.ValueType expectedType) {
3028-
wrap(node.operand, translator.topInfo.nullableType);
3029-
types.emitTypeCheck(this, node.type, dartTypeOf(node.operand), node);
3028+
final operandType = dartTypeOf(node.operand);
3029+
final boxedOperandType = operandType.isPotentiallyNullable
3030+
? translator.topInfo.nullableType
3031+
: translator.topInfo.nonNullableType;
3032+
wrap(node.operand, boxedOperandType);
3033+
types.emitIsTest(this, node.type, operandType, node);
30303034
return w.NumType.i32;
30313035
}
30323036

@@ -3036,23 +3040,13 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
30363040
return wrap(node.operand, expectedType);
30373041
}
30383042

3039-
w.Label asCheckBlock = b.block();
3040-
wrap(node.operand, translator.topInfo.nullableType);
3041-
w.Local operand = addLocal(translator.topInfo.nullableType);
3042-
b.local_tee(operand);
3043-
3044-
// We lower an `as` expression to a type test, throwing a [TypeError] if
3045-
// the type test fails.
3046-
types.emitTypeCheck(this, node.type, dartTypeOf(node.operand), node);
3047-
b.br_if(asCheckBlock);
3048-
b.local_get(operand);
3049-
types.makeType(this, node.type);
3050-
call(translator.stackTraceCurrent.reference);
3051-
call(translator.throwAsCheckError.reference);
3052-
b.unreachable();
3053-
b.end();
3054-
b.local_get(operand);
3055-
return operand.type;
3043+
final operandType = dartTypeOf(node.operand);
3044+
final boxedOperandType = operandType.isPotentiallyNullable
3045+
? translator.topInfo.nullableType
3046+
: translator.topInfo.nonNullableType;
3047+
wrap(node.operand, boxedOperandType);
3048+
return types.emitAsCheck(
3049+
this, node.type, operandType, boxedOperandType, node);
30563050
}
30573051

30583052
@override
@@ -3793,7 +3787,7 @@ enum _VirtualCallKind {
37933787
extension MacroAssembler on w.InstructionsBuilder {
37943788
// Expects there to be a i32 on the stack, will consume it and leave
37953789
// true/false on the stack.
3796-
void emitClassIdRangeCheck(CodeGenerator codeGen, List<Range> ranges) {
3790+
void emitClassIdRangeCheck(List<Range> ranges) {
37973791
if (ranges.isEmpty) {
37983792
drop();
37993793
i32_const(0);
@@ -3809,7 +3803,7 @@ extension MacroAssembler on w.InstructionsBuilder {
38093803
i32_lt_u();
38103804
}
38113805
} else {
3812-
w.Local idLocal = codeGen.addLocal(w.NumType.i32);
3806+
w.Local idLocal = addLocal(w.NumType.i32, isParameter: false);
38133807
local_set(idLocal);
38143808
w.Label done = block(const [], const [w.NumType.i32]);
38153809
i32_const(1);

pkg/dart2wasm/lib/intrinsics.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,7 @@ class Intrinsifier {
649649

650650
codeGen.wrap(classId, w.NumType.i64);
651651
b.i32_wrap_i64();
652-
b.emitClassIdRangeCheck(
653-
codeGen, [Range(objectClassId, objectClassId)]);
652+
b.emitClassIdRangeCheck([Range(objectClassId, objectClassId)]);
654653
return w.NumType.i32;
655654
case "_isClosureClassId":
656655
final classId = node.arguments.positional.single;
@@ -661,7 +660,7 @@ class Intrinsifier {
661660

662661
codeGen.wrap(classId, w.NumType.i64);
663662
b.i32_wrap_i64();
664-
b.emitClassIdRangeCheck(codeGen, ranges);
663+
b.emitClassIdRangeCheck(ranges);
665664
return w.NumType.i32;
666665
case "_isRecordClassId":
667666
final classId = node.arguments.positional.single;
@@ -672,7 +671,7 @@ class Intrinsifier {
672671

673672
codeGen.wrap(classId, w.NumType.i64);
674673
b.i32_wrap_i64();
675-
b.emitClassIdRangeCheck(codeGen, ranges);
674+
b.emitClassIdRangeCheck(ranges);
676675
return w.NumType.i32;
677676
}
678677
}

pkg/dart2wasm/lib/types.dart

Lines changed: 146 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -567,17 +567,19 @@ class Types {
567567
/// Emit code for testing a value against a Dart type. Expects the value on
568568
/// the stack as a (ref null #Top) and leaves the result on the stack as an
569569
/// i32.
570-
void emitTypeCheck(CodeGenerator codeGen, DartType type, DartType operandType,
570+
void emitIsTest(CodeGenerator codeGen, DartType type, DartType operandType,
571571
[TreeNode? node]) {
572572
final b = codeGen.b;
573573
b.comment("Type check against $type");
574574
w.Local? operandTemp;
575575
if (translator.options.verifyTypeChecks) {
576-
operandTemp = codeGen.addLocal(translator.topInfo.nullableType);
576+
operandTemp =
577+
b.addLocal(translator.topInfo.nullableType, isParameter: false);
577578
b.local_tee(operandTemp);
578579
}
579-
if (!_emitOptimizedTypeCheck(codeGen, type, operandType)) {
580-
// General fallback path
580+
if (_canUseIsCheckHelper(codeGen, type, operandType)) {
581+
b.call(_generateIsChecker(type as InterfaceType, operandType));
582+
} else {
581583
makeType(codeGen, type);
582584
codeGen.call(translator.isSubtype.reference);
583585
}
@@ -597,15 +599,36 @@ class Types {
597599
}
598600
}
599601

600-
/// Emit optimized code for testing a value against a Dart type. If the type
601-
/// to be tested against is of a shape where we can generate more efficient
602-
/// code than the general fallback path, generate such code and return `true`.
603-
/// Otherwise, return `false` to indicate that the general path should be
604-
/// taken.
605-
bool _emitOptimizedTypeCheck(
602+
w.ValueType emitAsCheck(CodeGenerator codeGen, DartType type,
603+
DartType operandType, w.RefType boxedOperandType,
604+
[TreeNode? node]) {
605+
final b = codeGen.b;
606+
607+
if (_canUseAsCheckHelper(codeGen, type, operandType)) {
608+
b.call(_generateAsChecker(type as InterfaceType, operandType));
609+
return translator.translateType(type);
610+
}
611+
612+
w.Local operand = b.addLocal(boxedOperandType, isParameter: false);
613+
b.local_tee(operand);
614+
w.Label asCheckBlock = b.block();
615+
b.local_get(operand);
616+
emitIsTest(codeGen, type, operandType, node);
617+
b.br_if(asCheckBlock);
618+
b.local_get(operand);
619+
makeType(codeGen, type);
620+
codeGen.call(translator.stackTraceCurrent.reference);
621+
codeGen.call(translator.throwAsCheckError.reference);
622+
b.unreachable();
623+
b.end();
624+
return operand.type;
625+
}
626+
627+
bool _canUseIsCheckHelper(
606628
CodeGenerator codeGen, DartType type, DartType operandType) {
607-
if (type is! InterfaceType) return false;
629+
if (_canUseAsCheckHelper(codeGen, type, operandType)) return true;
608630

631+
if (type is! InterfaceType) return false;
609632
if (type.typeArguments.any((t) => t is! DynamicType)) {
610633
// Type has at least one type argument that is not `dynamic`.
611634
//
@@ -627,43 +650,124 @@ class Types {
627650
return false;
628651
}
629652
}
653+
return true;
654+
}
630655

631-
final b = codeGen.b;
632-
bool isPotentiallyNullable = operandType.isPotentiallyNullable;
633-
w.Label? resultLabel;
634-
if (isPotentiallyNullable) {
635-
// Store operand in a temporary variable, since Binaryen does not support
636-
// block inputs.
637-
w.Local operand = codeGen.addLocal(translator.topInfo.nullableType);
638-
b.local_set(operand);
639-
resultLabel = b.block(const [], const [w.NumType.i32]);
640-
w.Label nullLabel = b.block(const [], const []);
641-
b.local_get(operand);
642-
b.br_on_null(nullLabel);
643-
}
656+
bool _canUseAsCheckHelper(
657+
CodeGenerator codeGen, DartType type, DartType operandType) {
658+
if (type is! InterfaceType) return false;
659+
// TODO: Should be rather defaults to bounds instead of `dynamic`.
660+
// (assuming omitting bound will make it dynamic rather than void/Object?)
661+
return type.typeArguments.every((t) => t is DynamicType);
662+
}
663+
664+
final Map<DartType, w.BaseFunction> _nullableIsCheckers = {};
665+
final Map<DartType, w.BaseFunction> _isCheckers = {};
644666

667+
w.BaseFunction _generateIsChecker(InterfaceType type, DartType operandType) {
668+
final operandIsNullable = operandType.isPotentiallyNullable;
645669
final interfaceClass = type.classNode;
646670

647-
if (interfaceClass == coreTypes.objectClass) {
648-
b.drop();
649-
b.i32_const(1);
650-
} else if (interfaceClass == coreTypes.functionClass) {
651-
b.ref_test(translator.closureInfo.nonNullableType);
652-
} else {
653-
final ranges =
654-
translator.classIdNumbering.getConcreteClassIdRanges(interfaceClass);
655-
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
656-
b.emitClassIdRangeCheck(codeGen, ranges);
657-
}
671+
final cachedIsCheckers =
672+
operandIsNullable ? _nullableIsCheckers : _isCheckers;
673+
674+
return cachedIsCheckers.putIfAbsent(type, () {
675+
final argumentType = operandIsNullable
676+
? translator.topInfo.nullableType
677+
: translator.topInfo.nonNullableType;
678+
final function = translator.m.functions.define(
679+
translator.m.types.defineFunction(
680+
[argumentType],
681+
[w.NumType.i32],
682+
),
683+
'<obj> is <$type>');
684+
685+
final b = function.body;
686+
b.local_get(b.locals[0]);
687+
688+
w.Label? resultLabel;
689+
if (operandIsNullable) {
690+
// Store operand in a temporary variable, since Binaryen does not support
691+
// block inputs.
692+
w.Local operand = function.addLocal(translator.topInfo.nullableType);
693+
b.local_set(operand);
694+
resultLabel = b.block(const [], const [w.NumType.i32]);
695+
w.Label nullLabel = b.block(const [], const []);
696+
b.local_get(operand);
697+
b.br_on_null(nullLabel);
698+
}
658699

659-
if (isPotentiallyNullable) {
660-
b.br(resultLabel!);
661-
b.end(); // nullLabel
662-
b.i32_const(encodedNullability(type));
663-
b.end(); // resultLabel
664-
}
700+
if (interfaceClass == coreTypes.objectClass) {
701+
b.drop();
702+
b.i32_const(1);
703+
} else if (interfaceClass == coreTypes.functionClass) {
704+
b.ref_test(translator.closureInfo.nonNullableType);
705+
} else {
706+
final ranges = translator.classIdNumbering
707+
.getConcreteClassIdRanges(interfaceClass);
708+
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
709+
b.emitClassIdRangeCheck(ranges);
710+
}
665711

666-
return true;
712+
if (operandIsNullable) {
713+
b.br(resultLabel!);
714+
b.end(); // nullLabel
715+
b.i32_const(encodedNullability(type));
716+
b.end(); // resultLabel
717+
}
718+
719+
b.return_();
720+
b.end();
721+
722+
return function;
723+
});
724+
}
725+
726+
final Map<DartType, w.BaseFunction> _nullableAsCheckers = {};
727+
final Map<DartType, w.BaseFunction> _asCheckers = {};
728+
729+
w.BaseFunction _generateAsChecker(InterfaceType type, DartType operandType) {
730+
final operandIsNullable = operandType.isPotentiallyNullable;
731+
final cachedAsCheckers =
732+
operandIsNullable ? _nullableAsCheckers : _asCheckers;
733+
734+
final returnType = translator.translateType(type);
735+
736+
return cachedAsCheckers.putIfAbsent(type, () {
737+
final argumentType = operandIsNullable
738+
? translator.topInfo.nullableType
739+
: translator.topInfo.nonNullableType;
740+
final function = translator.m.functions.define(
741+
translator.m.types.defineFunction(
742+
[argumentType],
743+
[returnType],
744+
),
745+
'<obj> as <$type>');
746+
747+
final b = function.body;
748+
w.Label asCheckBlock = b.block();
749+
b.local_get(b.locals[0]);
750+
b.call(_generateIsChecker(type, operandType));
751+
b.br_if(asCheckBlock);
752+
753+
b.local_get(b.locals[0]);
754+
translator.constants.instantiateConstant(
755+
function, b, TypeLiteralConstant(type), nonNullableTypeType);
756+
b.call(translator.functions
757+
.getFunction(translator.stackTraceCurrent.reference));
758+
b.call(translator.functions
759+
.getFunction(translator.throwAsCheckError.reference));
760+
b.unreachable();
761+
762+
b.end();
763+
764+
b.local_get(b.locals[0]);
765+
translator.convertType(function, argumentType, returnType);
766+
b.return_();
767+
b.end();
768+
769+
return function;
770+
});
667771
}
668772

669773
int encodedNullability(DartType type) =>

pkg/wasm_builder/lib/src/builder/instructions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ class InstructionsBuilder with Builder<ir.Instructions> {
251251
return _stackTypes.sublist(_stackTypes.length - n);
252252
}
253253

254+
List<ir.ValueType> get stack => _stackTypes;
255+
254256
List<ir.ValueType> _checkStackTypes(List<ir.ValueType> inputs,
255257
[List<ir.ValueType>? stack]) {
256258
stack ??= _stack(inputs.length);

0 commit comments

Comments
 (0)