Skip to content

Commit 2254755

Browse files
natebiggsCommit Queue
authored andcommitted
[dart2wasm] Generate br_table for some switch statements.
Often switches over enums are exhaustive or nearly exhaustive. The current generator uses identity for the enum values which requires iterating over each value to compare it to the test expression. So each time the switch body is entered is an O(n) operation (where is n is the # of case expressions). This now generates a br_table using the index of the enum and jumps directly to the correct clause, effectively an O(1) operation. A similar approach is taken for switches over an integer range. If the range of case expresison values is close in size to the # of values, it is advantageous to normalize the range around 0 and treat the values themselves as table indices. This approach will also save code size when the index range is dense as the br_table is more compact than the identity/br_if checks. For sparse ranges this may produce a bit more code though only on the order of a few bytes per value in the range. I've added a denseness heuristic to decide when to revert to the current strategy to avoid the code size penalty. Golem benchmark: https://golem.corp.goog/Comparison?repository=dart#targetA%3Ddart2wasm-O2-d8%3BmachineTypeA%3Dlinux-x64%3BrevisionA%3D117402%3BpatchA%3Dnatebiggs-dart2wasm-switch-tables3%3BtargetB%3Ddart2wasm-O2-d8%3BmachineTypeB%3Dlinux-x64%3BrevisionB%3D117401%3BpatchB%3DNone See 100% improvement in SwitchFSM.int and 90% improvement in SwitchFSM.enum. Change-Id: Ie29e8fd59ef6235044ba5b4a4af04023d702ce57 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441760 Reviewed-by: Ömer Ağacan <[email protected]> Commit-Queue: Nate Biggs <[email protected]>
1 parent 02ea7d0 commit 2254755

File tree

2 files changed

+181
-28
lines changed

2 files changed

+181
-28
lines changed

pkg/dart2wasm/lib/code_generator.dart

Lines changed: 179 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,29 +1424,44 @@ abstract class AstCodeGenerator
14241424
b.end();
14251425
}
14261426

1427-
// Compare against all case values
1428-
for (SwitchCase c in node.cases) {
1429-
for (Expression exp in c.expressions) {
1430-
if (exp is NullLiteral ||
1431-
exp is ConstantExpression && exp.constant is NullConstant) {
1432-
// Null already checked, skip
1433-
} else {
1434-
switchInfo.compare(
1435-
switchValueNonNullableLocal,
1436-
() => translateExpression(exp, switchInfo.nonNullableType),
1437-
);
1438-
b.br_if(switchLabels[c]!);
1427+
final brTable = switchInfo.brTable;
1428+
if (brTable != null) {
1429+
// Map each entry in the range to the appropriate jump table entry.
1430+
final indexBlocks = <w.Label>[];
1431+
final caseMap = brTable.caseMap;
1432+
final defaultLabel =
1433+
defaultCase != null ? switchLabels[defaultCase]! : doneLabel;
1434+
for (int i = 0; i < brTable.rangeSize; i++) {
1435+
final c = caseMap[i];
1436+
indexBlocks.add(c == null ? defaultLabel : switchLabels[c]!);
1437+
}
1438+
brTable.brTableExpr(switchValueNonNullableLocal);
1439+
b.br_table(indexBlocks, defaultLabel);
1440+
} else {
1441+
// Compare against all case values
1442+
for (SwitchCase c in node.cases) {
1443+
for (Expression exp in c.expressions) {
1444+
if (exp is NullLiteral ||
1445+
exp is ConstantExpression && exp.constant is NullConstant) {
1446+
// Null already checked, skip
1447+
} else {
1448+
switchInfo.compare(
1449+
switchValueNonNullableLocal,
1450+
() => translateExpression(exp, switchInfo.nonNullableType),
1451+
);
1452+
b.br_if(switchLabels[c]!);
1453+
}
14391454
}
14401455
}
1441-
}
14421456

1443-
// No explicit cases matched
1444-
if (node.isExplicitlyExhaustive) {
1445-
b.unreachable();
1446-
} else {
1447-
w.Label defaultLabel =
1448-
defaultCase != null ? switchLabels[defaultCase]! : doneLabel;
1449-
b.br(defaultLabel);
1457+
// No explicit cases matched
1458+
if (node.isExplicitlyExhaustive) {
1459+
b.unreachable();
1460+
} else {
1461+
w.Label defaultLabel =
1462+
defaultCase != null ? switchLabels[defaultCase]! : doneLabel;
1463+
b.br(defaultLabel);
1464+
}
14501465
}
14511466

14521467
// Emit case bodies
@@ -4297,6 +4312,47 @@ class SwitchBackwardJumpInfo {
42974312
: defaultLoopLabel = null;
42984313
}
42994314

4315+
/// Info needed to represent a switch statement using a br_table instruction.
4316+
///
4317+
/// This is used for switches on integers and enums (using their indicies). We
4318+
/// map each option to an index in the jump table and then jump directly to the
4319+
/// target case. This is much faster than iteratively comparing each cases's
4320+
/// expression to the switch expression.
4321+
///
4322+
/// Sometimes switches over ranges of ints are sparse enough that a table would
4323+
/// bloat the code compared to the iterative comparison.
4324+
class BrTableInfo {
4325+
// This is the minimum ratio of br_table indicies to actual mapped cases. If
4326+
// this gets too large then most of the br_table indicies are unmapped and the
4327+
// extra code size is not worth using a br_table.
4328+
static const double _minTableSparseness = 2;
4329+
// This is the maximum size where it's always worth it to use a br_table.
4330+
// If the br_table is bigger than this then we start to check sparseness.
4331+
// Below this point we always accept the potential code size hit.
4332+
static const int _maxSparseSize = 50;
4333+
4334+
final int rangeSize;
4335+
final void Function(w.Local switchExprLocal) brTableExpr;
4336+
final Map<int, SwitchCase> caseMap;
4337+
4338+
BrTableInfo._(this.rangeSize, this.caseMap, this.brTableExpr)
4339+
: assert(!isTooSparse(rangeSize, caseMap));
4340+
4341+
/// Heuristically validate whether the provided table would be too sparse and
4342+
/// if so return null. Otherwise return the expected table.
4343+
static BrTableInfo? build(int rangeSize, Map<int, SwitchCase> caseMap,
4344+
void Function(w.Local switchExprLocal) brTableExpr) {
4345+
// Validate the table density and size is worth putting into a br_table.
4346+
if (isTooSparse(rangeSize, caseMap)) return null;
4347+
4348+
return BrTableInfo._(rangeSize, caseMap, brTableExpr);
4349+
}
4350+
4351+
static bool isTooSparse(int rangeSize, Map<int, SwitchCase> caseMap) =>
4352+
(rangeSize / caseMap.length) > _minTableSparseness &&
4353+
rangeSize > _maxSparseSize;
4354+
}
4355+
43004356
class SwitchInfo {
43014357
/// Non-nullable Wasm type of the `switch` expression. Used when the
43024358
/// expression is not nullable, and after the null check.
@@ -4323,6 +4379,11 @@ class SwitchInfo {
43234379
/// The `null: ...` case, if exists.
43244380
late final SwitchCase? nullCase;
43254381

4382+
/// Info needed to compile this switch statement into a wasm br_table. If null
4383+
/// this switch statement should not use a br_table and should use comparison
4384+
/// based case matching instead.
4385+
BrTableInfo? brTable;
4386+
43264387
SwitchInfo(AstCodeGenerator codeGen, SwitchStatement node) {
43274388
final translator = codeGen.translator;
43284389

@@ -4354,6 +4415,17 @@ class SwitchInfo {
43544415
translator.typeEnvironment.isSubtypeOf(codeGen.dartTypeOf(e),
43554416
translator.coreTypes.typeNonNullableRawType));
43564417

4418+
void useCompareIdentity() {
4419+
// Object identity switch
4420+
nonNullableType = translator.topTypeNonNullable;
4421+
nullableType = translator.topType;
4422+
compare = (switchExprLocal, pushCaseExpr) {
4423+
codeGen.b.local_get(switchExprLocal);
4424+
pushCaseExpr();
4425+
codeGen.call(translator.coreTypes.identicalProcedure.reference);
4426+
};
4427+
}
4428+
43574429
if (node.cases.every((c) =>
43584430
c.expressions.isEmpty && c.isDefault ||
43594431
c.expressions.every((e) =>
@@ -4453,6 +4525,39 @@ class SwitchInfo {
44534525
nonNullableType = w.NumType.i64;
44544526
nullableType =
44554527
translator.classInfo[translator.boxedIntClass]!.nullableType;
4528+
4529+
// Calculate the range covered by the cases and create the jump table.
4530+
int? minValue;
4531+
int? maxValue;
4532+
Map<int, SwitchCase> caseMap = {};
4533+
for (final c in node.cases) {
4534+
for (final e in c.expressions) {
4535+
final value = e is IntLiteral
4536+
? e.value
4537+
: ((e as ConstantExpression).constant as IntConstant).value;
4538+
caseMap[value] = c;
4539+
if (minValue == null || value < minValue) minValue = value;
4540+
if (maxValue == null || value > maxValue) maxValue = value;
4541+
}
4542+
}
4543+
if (maxValue != null) {
4544+
final range = maxValue - minValue! + 1;
4545+
if (minValue != 0) {
4546+
caseMap = caseMap.map((i, c) => MapEntry(i - minValue!, c));
4547+
}
4548+
brTable = BrTableInfo.build(range, caseMap, (switchExprLocal) {
4549+
codeGen.b.local_get(switchExprLocal);
4550+
// Normalize the value on 0 if necessary.
4551+
if (minValue != 0) {
4552+
codeGen.b.i64_const(minValue!);
4553+
codeGen.b.i64_sub();
4554+
}
4555+
// Now that we've normalized to 0 it should be safe to switch to i32.
4556+
codeGen.b.i32_wrap_i64();
4557+
});
4558+
}
4559+
4560+
// Provide a compare as a fallback in case the range is too sparse.
44564561
compare = (switchExprLocal, pushCaseExpr) {
44574562
codeGen.b.local_get(switchExprLocal);
44584563
pushCaseExpr();
@@ -4467,15 +4572,61 @@ class SwitchInfo {
44674572
pushCaseExpr();
44684573
codeGen.call(translator.jsStringEquals.reference);
44694574
};
4575+
} else if (switchExprClass.isEnum) {
4576+
// If this is an applicable switch over enums, create a jump table.
4577+
bool isValid = true;
4578+
final caseMap = <int, SwitchCase>{};
4579+
int? minIndex;
4580+
int maxIndex = 0;
4581+
outer:
4582+
for (final c in node.cases) {
4583+
for (final e in c.expressions) {
4584+
if (e is! ConstantExpression) {
4585+
isValid = false;
4586+
break outer;
4587+
}
4588+
final constant = e.constant;
4589+
if (constant is! InstanceConstant) {
4590+
isValid = false;
4591+
break outer;
4592+
}
4593+
if (constant.classNode != switchExprClass) {
4594+
isValid = false;
4595+
break outer;
4596+
}
4597+
final enumIndex =
4598+
(constant.fieldValues[translator.enumIndexField.fieldReference]
4599+
as IntConstant)
4600+
.value;
4601+
caseMap[enumIndex] = c;
4602+
if (enumIndex > maxIndex) maxIndex = enumIndex;
4603+
if (minIndex == null || enumIndex < minIndex) minIndex = enumIndex;
4604+
}
4605+
}
4606+
4607+
if (isValid && minIndex != null) {
4608+
final range = maxIndex - minIndex + 1;
4609+
brTable = BrTableInfo.build(range, caseMap, (switchExprLocal) {
4610+
codeGen.b.local_get(switchExprLocal);
4611+
codeGen.call(translator.enumIndexField.getterReference);
4612+
if (minIndex != 0) {
4613+
codeGen.b.i64_const(minIndex!);
4614+
codeGen.b.i64_sub();
4615+
}
4616+
// Now that we've normalized to 0 it should be safe to switch to i32.
4617+
codeGen.b.i32_wrap_i64();
4618+
});
4619+
4620+
nonNullableType =
4621+
translator.classInfo[switchExprClass]!.nonNullableType;
4622+
nullableType = translator.classInfo[switchExprClass]!.nullableType;
4623+
}
4624+
4625+
if (brTable == null) {
4626+
useCompareIdentity();
4627+
}
44704628
} else {
4471-
// Object identity switch
4472-
nonNullableType = translator.topTypeNonNullable;
4473-
nullableType = translator.topType;
4474-
compare = (switchExprLocal, pushCaseExpr) {
4475-
codeGen.b.local_get(switchExprLocal);
4476-
pushCaseExpr();
4477-
codeGen.call(translator.coreTypes.identicalProcedure.reference);
4478-
};
4629+
useCompareIdentity();
44794630
}
44804631

44814632
_initializeSpecialCases(node);

pkg/dart2wasm/lib/kernel_nodes.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ mixin KernelNodes {
5757
late final Class typeErrorClass = index.getClass("dart:core", "_TypeError");
5858
late final Class javaScriptErrorClass =
5959
index.getClass("dart:core", "_JavaScriptError");
60+
late final Field enumIndexField =
61+
index.getField('dart:core', '_Enum', 'index');
6062

6163
// dart:core runtime type classes
6264
late final Class typeClass = index.getClass("dart:core", "_Type");

0 commit comments

Comments
 (0)