From c833c1227ccdbf587fb1fc120efda2f8745148b4 Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Wed, 5 Feb 2025 04:57:00 -0800 Subject: [PATCH] [dart2wasm] Add support for dispatch table calls to unchecked entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that we have support for generating checked & unchecked entrypoints, we can make dispatch tables also target unchecked entry points. This is beneficial especially in cases where there's dispatches on `this` that require covariant checks but we don't know the target method (i.e. we cannot devirtualize it because the method we dispatch to may beoverriden). We keep the existing selectors that we have, but a selector will now have * one row if none of the implementations of the selector need to perform type checks => `SelectorInfo` has a `SelectorTargets _normal` * two rows if any of the implementations of the selector need t perform a type check => `SelectorInfo` has a `SelectorTargets _checked` => `SelectorInfo` has a `SelectorTargets _unchecked` Once an unchecked entrypoint is also used in the dispatch table (only if there's any unchecked calls to that selector) then binaryen can no longer optimize the signature of the function. It means we may have perform e.g. downcasts / boxing in the unchecked entry where we wouldn't do before (because we only had static calls to unchecked entry before this PR). So we're going to force-inline calls to unchecked entrypoints. This avoids sometimes down casts and boxing. It also seems to actually shrink the binary size. Change-Id: I3ba4980c42886cc883fb610533f5fac9cce39b65 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/407740 Reviewed-by: Ömer Ağacan Commit-Queue: Martin Kustermann --- pkg/dart2wasm/lib/code_generator.dart | 62 ++++--- pkg/dart2wasm/lib/dispatch_table.dart | 178 +++++++++++++++----- pkg/dart2wasm/lib/dynamic_forwarders.dart | 23 ++- pkg/dart2wasm/lib/functions.dart | 10 +- pkg/dart2wasm/lib/reference_extensions.dart | 12 +- pkg/dart2wasm/lib/translator.dart | 82 +++++---- 6 files changed, 242 insertions(+), 125 deletions(-) diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index 4767e3839b1..a09765e1425 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -670,11 +670,13 @@ abstract class AstCodeGenerator List call(Reference target) { final targetModule = translator.moduleForReference(target); final isLocalModuleCall = targetModule == b.module; + final name = translator.functions.getFunctionName(target); if (isLocalModuleCall) { + b.comment('Direct call to $name'); return b.invoke(translator.directCallTarget(target)); } else { - b.comment('Indirect call to $target'); + b.comment('Direct call to $name (across modules)'); return translator.callReference(target, b); } } @@ -1669,7 +1671,7 @@ abstract class AstCodeGenerator translateExpression(node.receiver, signature.inputs.first), (w.FunctionType signature, ParameterInfo paramInfo) { _visitArguments(node.arguments, signature, paramInfo, 1); - }); + }, useUncheckedEntry: useUncheckedEntry); } @override @@ -1808,11 +1810,13 @@ abstract class AstCodeGenerator } } + final useUncheckedEntry = + translator.canUseUncheckedEntry(node.left, node); if (singleTarget != null) { left(); right(); call(translator.getFunctionEntry(singleTarget.reference, - uncheckedEntry: translator.canUseUncheckedEntry(node, node.left))); + uncheckedEntry: useUncheckedEntry)); } else { _virtualCall( node, @@ -1820,6 +1824,7 @@ abstract class AstCodeGenerator _VirtualCallKind.Call, left, right, + useUncheckedEntry: useUncheckedEntry, ); } if (leftNullable || rightNullable) { @@ -1853,33 +1858,34 @@ abstract class AstCodeGenerator _VirtualCallKind kind, void Function(w.FunctionType signature) pushReceiver, void Function(w.FunctionType signature, ParameterInfo) pushArguments, - {bool useUncheckedEntry = false}) { + {required bool useUncheckedEntry}) { assert(kind != _VirtualCallKind.Get || !useUncheckedEntry); SelectorInfo selector = translator.dispatchTable.selectorForTarget( interfaceTarget.referenceAs( getter: kind.isGetter, setter: kind.isSetter)); + final name = selector.entryPointName(useUncheckedEntry); assert(selector.name == interfaceTarget.name.text); pushReceiver(selector.signature); - if (selector.targetRanges.length == 1) { + final targets = selector.targets(unchecked: useUncheckedEntry); + + if (targets.targetRanges.length == 1) { // TODO(natebiggs): Ensure dynamic modules exclude this. - assert(selector.staticDispatchRanges.length == 1); - final target = translator.getFunctionEntry( - selector.targetRanges[0].target, - uncheckedEntry: useUncheckedEntry); + assert(targets.staticDispatchRanges.length == 1); + final target = targets.targetRanges.single.target; final signature = translator.signatureForDirectCall(target); final paramInfo = translator.paramInfoForDirectCall(target); pushArguments(signature, paramInfo); return translator.outputOrVoid(call(target)); } - if (selector.targetRanges.isEmpty) { + if (targets.targetRanges.isEmpty) { // TODO(natebiggs): Ensure dynamic modules exclude this. // Unreachable call - b.comment("Virtual call of ${selector.name} with no targets" + b.comment("Virtual call of $name with no targets" " at ${node.location}"); pushArguments(selector.signature, selector.paramInfo); for (int i = 0; i < selector.signature.inputs.length; ++i) { @@ -1897,16 +1903,18 @@ abstract class AstCodeGenerator b.local_tee(receiverVar); pushArguments(selector.signature, selector.paramInfo); - if (selector.staticDispatchRanges.isNotEmpty) { + if (targets.staticDispatchRanges.isNotEmpty) { // TODO(natebiggs): Ensure dynamic modules exclude this. b.invoke(translator .getPolymorphicDispatchersForModule(b.module) - .getPolymorphicDispatcher(selector)); + .getPolymorphicDispatcher(selector, + useUncheckedEntry: useUncheckedEntry)); } else { - b.comment("Instance $kind of '${selector.name}'"); + b.comment("Instance $kind of '$name'"); b.local_get(receiverVar); - translator.callDispatchTable(b, selector); + translator.callDispatchTable(b, selector, + useUncheckedEntry: useUncheckedEntry); } return translator.outputOrVoid(selector.signature.outputs); @@ -2039,7 +2047,7 @@ abstract class AstCodeGenerator w.Label nullLabel = b.block(); translateExpression(node.receiver, translator.topInfo.nullableType); b.br_on_null(nullLabel); - }, (_, __) {}); + }, (_, __) {}, useUncheckedEntry: false); b.br(doneLabel); b.end(); // nullLabel switch (target.name.text) { @@ -2069,7 +2077,8 @@ abstract class AstCodeGenerator _VirtualCallKind.Get, (signature) => translateExpression(node.receiver, signature.inputs.first), - (_, __) {}); + (_, __) {}, + useUncheckedEntry: false); } } @@ -2181,7 +2190,7 @@ abstract class AstCodeGenerator b.br_on_null(nullLabel); translator.convertType( b, translator.topInfo.nullableType, signature.inputs[0]); - }, (_, __) {}); + }, (_, __) {}, useUncheckedEntry: false); b.br(doneLabel); b.end(); // nullLabel switch (target.name.text) { @@ -2212,7 +2221,8 @@ abstract class AstCodeGenerator _VirtualCallKind.Get, (signature) => translateExpression(node.receiver, signature.inputs.first), - (_, __) {}); + (_, __) {}, + useUncheckedEntry: false); } @override @@ -3187,13 +3197,13 @@ CodeGenerator? getInlinableMemberCodeGenerator(Translator translator, class SynchronousProcedureCodeGenerator extends AstCodeGenerator { final Procedure member; - final SynchronousProcedureKind kind; + final EntryPoint kind; SynchronousProcedureCodeGenerator(Translator translator, w.FunctionType functionType, this.member, this.kind) : super(translator, functionType, member) { - assert(!translator.needToCheckTypesFor(member) || - kind != SynchronousProcedureKind.normal); + assert( + !translator.needToCheckTypesFor(member) || kind != EntryPoint.normal); } @override @@ -3216,16 +3226,16 @@ class SynchronousProcedureCodeGenerator extends AstCodeGenerator { closures = translator.getClosures(member); switch (kind) { - case SynchronousProcedureKind.normal: + case EntryPoint.normal: b.comment('Normal Entry'); _makeNonMultiEntryPointFunction(); - case SynchronousProcedureKind.checked: + case EntryPoint.checked: b.comment('Checked Entry'); _makeMultipleEntryPoint(true); - case SynchronousProcedureKind.unchecked: + case EntryPoint.unchecked: b.comment('Unchecked Entry'); _makeMultipleEntryPoint(false); - case SynchronousProcedureKind.body: + case EntryPoint.body: b.comment('Body for Checked & Unchecked Entry'); _makeMultipleEntryPointSharedBody(); break; diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart index 751b6e0abbb..506c24452b2 100644 --- a/pkg/dart2wasm/lib/dispatch_table.dart +++ b/pkg/dart2wasm/lib/dispatch_table.dart @@ -39,21 +39,11 @@ class SelectorInfo { /// Is this an implicit or explicit setter? final bool isSetter; - /// Does this method have any tear-off uses? - bool hasTearOffUses = false; - - /// Does this method have any non-this uses? - bool hasNonThisUses = false; - - /// Targets for all concrete classes implementing this selector. + /// Whether we create multiple entry points for the selector. /// - /// As a subclass hierarchy often inherits the same target, we associate the - /// target with a range of class ids. The ranges are non-empty, - /// non-overlapping and sorted in ascending order. - late final List<({Range range, Reference target})> targetRanges; - late final Set targetSet = - targetRanges.map((e) => e.target).toSet(); - late final List<({Range range, Reference target})> staticDispatchRanges; + /// We create multiple entry points when any implementation of this selector + /// performs type checks on the passed arguments. + bool useMultipleEntryPoints = false; /// Wasm function type for the selector. /// @@ -63,12 +53,6 @@ class SelectorInfo { /// Number of concrete classes that provide this selector. late final int concreteClasses; - /// Offset of the selector in the dispatch table. - /// - /// For a class in [targetRanges], `class ID + offset` gives the offset of the - /// class member for this selector. - int? offset; - /// The selector's member's name. final String name; @@ -77,10 +61,30 @@ class SelectorInfo { /// `noSuchMethod` overrides to the dispatch table. final bool isNoSuchMethod; + late final SelectorTargets? _normal; + late final SelectorTargets? _checked; + late final SelectorTargets? _unchecked; + + SelectorTargets targets({required bool unchecked}) { + if (useMultipleEntryPoints) { + assert(_checked!.targetRanges.length == _unchecked!.targetRanges.length); + assert(_checked!.staticDispatchRanges.length == + _unchecked!.staticDispatchRanges.length); + return unchecked ? _unchecked! : _checked!; + } + assert(_checked == null && _unchecked == null); + return _normal!; + } + SelectorInfo._( this.translator, this.id, this.name, this.callCount, this.paramInfo, {required this.isSetter, required this.isNoSuchMethod}); + String entryPointName(bool unchecked) { + if (!useMultipleEntryPoints) return name; + return '$name (${unchecked ? 'unchecked' : 'checked'})'; + } + /// Compute the signature for the functions implementing members targeted by /// this selector. /// @@ -95,7 +99,7 @@ class SelectorInfo { List.generate(1 + paramInfo.paramCount, (_) => {}); List> outputSets = List.generate(returnCount, (_) => {}); List ensureBoxed = List.filled(1 + paramInfo.paramCount, false); - for (final (range: _, :target) in targetRanges) { + for (final (range: _, :target) in targets(unchecked: false).targetRanges) { Member member = target.asMember; DartType receiver = InterfaceType(member.enclosingClass!, Nullability.nonNullable); @@ -214,6 +218,39 @@ class SelectorInfo { } return w.RefType.def(heapTypes.single, nullable: nullable); } + + late final Set targetSet = useMultipleEntryPoints + ? {..._unchecked!._targetSet, ..._checked!._targetSet} + : _normal!._targetSet; +} + +class SelectorTargets { + /// Targets for all concrete classes implementing this selector. + /// + /// As a subclass hierarchy often inherits the same target, we associate the + /// target with a range of class ids. The ranges are non-empty, + /// non-overlapping and sorted in ascending order. + final List<({Range range, Reference target})> targetRanges; + + /// Targets that a interface call will check & directly call before falling + /// back to dispatch table calls. + /// + /// The targets in here are mainly the ones annotated with + /// `@pragma('wasm:static-dispatch')`. The compiler will generate then code + /// that first checks the receiver for those targets directly and issue direct + /// calls before falling back to dispatch table calls. + final List<({Range range, Reference target})> staticDispatchRanges; + + /// Offset of the selector in the dispatch table. + /// + /// For a class in [targetRanges], `class ID + offset` gives the offset of the + /// class member for this selector. + late final int offset; + + SelectorTargets(this.targetRanges, this.staticDispatchRanges); + + late final Set _targetSet = + targetRanges.map((e) => e.target).toSet(); } /// Builds the dispatch table for member calls. @@ -256,9 +293,6 @@ class DispatchTable { _importedWasmTables = WasmTableImporter(translator, 'dispatch'); SelectorInfo selectorForTarget(Reference target) { - // Dispatch table currently doesn't have unchecked entries. - assert(!target.isUncheckedEntryReference); - Member member = target.asMember; bool isGetter = target.isGetter || target.isTearOffReference; ProcedureAttributesMetadata metadata = @@ -296,8 +330,11 @@ class DispatchTable { isSetter: isSetter, isNoSuchMethod: member == translator.objectNoSuchMethod)); assert(selector.isSetter == isSetter); - selector.hasTearOffUses |= metadata.hasTearOffUses; - selector.hasNonThisUses |= metadata.hasNonThisUses; + final useMultipleEntryPoints = !member.isAbstract && + !target.isGetter && + !target.isTearOffReference && + translator.needToCheckTypesFor(member); + selector.useMultipleEntryPoints |= useMultipleEntryPoints; selector.paramInfo.merge(paramInfo); if (calledDynamically) { if (isGetter) { @@ -382,13 +419,11 @@ class DispatchTable { if (member is Field) { addMember(member.getterReference, staticDispatch); if (member.hasSetter) { - final target = translator.getFunctionEntry(member.setterReference!, - uncheckedEntry: false); + final target = member.setterReference!; addMember(target, staticDispatch); } } else if (member is Procedure) { - final target = translator.getFunctionEntry(member.reference, - uncheckedEntry: false); + final target = member.reference; addMember(target, staticDispatch); // `hasTearOffUses` can be true for operators as well, even though // it's not possible to tear-off an operator. (no syntax for it) @@ -449,14 +484,48 @@ class DispatchTable { : ranges .where((range) => staticDispatchPragmas.contains(range.target)) .toList(); - selector.targetRanges = ranges; - selector.staticDispatchRanges = staticDispatchRanges; + + if (selector.useMultipleEntryPoints) { + ({Range range, Reference target}) getChecked( + ({Range range, Reference target}) targetRange, + bool unchecked, + ) => + ( + range: targetRange.range, + target: translator.getFunctionEntry(targetRange.target, + uncheckedEntry: unchecked) + ); + + selector._normal = null; + selector._checked = SelectorTargets( + ranges.map((r) => getChecked(r, false)).toList(), + staticDispatchRanges.map((r) => getChecked(r, false)).toList(), + ); + selector._unchecked = SelectorTargets( + ranges.map((r) => getChecked(r, true)).toList(), + staticDispatchRanges.map((r) => getChecked(r, true)).toList(), + ); + } else { + selector._normal = SelectorTargets(ranges, staticDispatchRanges); + selector._checked = null; + selector._unchecked = null; + } }); _selectorInfo.forEach((_, selector) { if (!selectorTargets.containsKey(selector)) { - selector.concreteClasses = 0; - selector.targetRanges = []; + // There are no concrete implementations for the given [selector]. + // But there may be an abstract interface target which is targed by a + // call. In this case the call should be unreachable. + if (selector.useMultipleEntryPoints) { + selector._normal = null; + selector._checked = SelectorTargets([], []); + selector._unchecked = SelectorTargets([], []); + } else { + selector._normal = SelectorTargets([], []); + selector._checked = null; + selector._unchecked = null; + } } }); @@ -467,9 +536,11 @@ class DispatchTable { return true; } if (selector.callCount == 0) return false; - if (selector.targetRanges.length <= 1) return false; - if (selector.staticDispatchRanges.length == - selector.targetRanges.length) { + + final targets = selector.targets(unchecked: false); + + if (targets.targetRanges.length <= 1) return false; + if (targets.staticDispatchRanges.length == targets.targetRanges.length) { return false; } return true; @@ -494,19 +565,36 @@ class DispatchTable { final rows = >[]; for (final selector in selectors) { - final rowValues = <({int index, Reference value})>[]; - for (final (:range, :target) in selector.targetRanges) { - for (int classId = range.start; classId <= range.end; ++classId) { - rowValues.add((index: classId, value: target)); + Row buildRow( + List<({Range range, Reference target})> targetRanges) { + final rowValues = <({int index, Reference value})>[]; + for (final (:range, :target) in targetRanges) { + for (int classId = range.start; classId <= range.end; ++classId) { + rowValues.add((index: classId, value: target)); + } } + rowValues.sort((a, b) => a.index.compareTo(b.index)); + return Row(rowValues); + } + + if (selector.useMultipleEntryPoints) { + rows.add(buildRow(selector._checked!.targetRanges)); + rows.add(buildRow(selector._unchecked!.targetRanges)); + } else { + rows.add(buildRow(selector._normal!.targetRanges)); } - rowValues.sort((a, b) => a.index.compareTo(b.index)); - rows.add(Row(rowValues)); } _table = buildRowDisplacementTable(rows); - for (int i = 0; i < rows.length; ++i) { - selectors[i].offset = rows[i].offset; + + int rowIndex = 0; + for (final selector in selectors) { + if (selector.useMultipleEntryPoints) { + selector._checked!.offset = rows[rowIndex++].offset; + selector._unchecked!.offset = rows[rowIndex++].offset; + } else { + selector._normal!.offset = rows[rowIndex++].offset; + } } _definedWasmTable = diff --git a/pkg/dart2wasm/lib/dynamic_forwarders.dart b/pkg/dart2wasm/lib/dynamic_forwarders.dart index 199ebcd62c9..c1419084320 100644 --- a/pkg/dart2wasm/lib/dynamic_forwarders.dart +++ b/pkg/dart2wasm/lib/dynamic_forwarders.dart @@ -99,8 +99,10 @@ class Forwarder { final selectors = translator.dispatchTable.dynamicGetterSelectors(memberName); final ranges = selectors - .expand((selector) => - selector.targetRanges.map((r) => (range: r.range, value: r.target))) + .expand((selector) => selector + .targets(unchecked: false) + .targetRanges + .map((r) => (range: r.range, value: r.target))) .toList(); ranges.sort((a, b) => a.range.start.compareTo(b.range.start)); @@ -145,8 +147,10 @@ class Forwarder { final selectors = translator.dispatchTable.dynamicSetterSelectors(memberName); final ranges = selectors - .expand((selector) => - selector.targetRanges.map((r) => (range: r.range, value: r.target))) + .expand((selector) => selector + .targets(unchecked: false) + .targetRanges + .map((r) => (range: r.range, value: r.target))) .toList(); ranges.sort((a, b) => a.range.start.compareTo(b.range.start)); @@ -197,7 +201,8 @@ class Forwarder { for (final selector in methodSelectors) { // Accumulates all class ID ranges that have the same target. final Map> targets = {}; - for (final (:range, :target) in selector.targetRanges) { + for (final (:range, :target) + in selector.targets(unchecked: false).targetRanges) { targets.putIfAbsent(target, () => []).add(range); } @@ -477,7 +482,8 @@ class Forwarder { translator.dispatchTable.dynamicGetterSelectors(memberName); final getterValueLocal = b.addLocal(translator.topInfo.nullableType); for (final selector in getterSelectors) { - for (final (:range, :target) in selector.targetRanges) { + for (final (:range, :target) + in selector.targets(unchecked: false).targetRanges) { for (int classId = range.start; classId <= range.end; ++classId) { final targetMember = target.asMember; // This loop checks getters and fields. Methods are considered in the @@ -734,7 +740,7 @@ void generateNoSuchMethodCall( ) { final SelectorInfo noSuchMethodSelector = translator.dispatchTable .selectorForTarget(translator.objectNoSuchMethod.reference); - translator.functions.recordSelectorUse(noSuchMethodSelector); + translator.functions.recordSelectorUse(noSuchMethodSelector, false); final noSuchMethodParamInfo = noSuchMethodSelector.paramInfo; final noSuchMethodWasmFunctionType = noSuchMethodSelector.signature; @@ -773,7 +779,8 @@ void generateNoSuchMethodCall( // Get class id for virtual call pushReceiver(); - translator.callDispatchTable(b, noSuchMethodSelector); + translator.callDispatchTable(b, noSuchMethodSelector, + useUncheckedEntry: false); } class ClassIdRange { diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index 2ee953b2dc3..e0065a49549 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -26,6 +26,7 @@ class FunctionCollector { final Map _exports = {}; // Selector IDs that are invoked via GDT. final Set _calledSelectors = {}; + final Set _calledUncheckedSelectors = {}; // Class IDs for classes that are allocated somewhere in the program final Set _allocatedClasses = {}; // For each class ID, which functions should be added to the compilation queue @@ -231,9 +232,12 @@ class FunctionCollector { } } - void recordSelectorUse(SelectorInfo selector) { - if (_calledSelectors.add(selector.id)) { - for (final (:range, :target) in selector.targetRanges) { + void recordSelectorUse(SelectorInfo selector, bool useUncheckedEntry) { + final set = + useUncheckedEntry ? _calledUncheckedSelectors : _calledSelectors; + if (set.add(selector.id)) { + for (final (:range, :target) + in selector.targets(unchecked: useUncheckedEntry).targetRanges) { for (int classId = range.start; classId <= range.end; ++classId) { if (_allocatedClasses.contains(classId)) { // Class declaring or inheriting member is allocated somewhere. diff --git a/pkg/dart2wasm/lib/reference_extensions.dart b/pkg/dart2wasm/lib/reference_extensions.dart index ea5d222aeaf..7fee964aa1d 100644 --- a/pkg/dart2wasm/lib/reference_extensions.dart +++ b/pkg/dart2wasm/lib/reference_extensions.dart @@ -101,17 +101,17 @@ extension IsCustomReference on Reference { bool get isConstructorBodyReference => _constructorBodyReference[asMember] == this; - SynchronousProcedureKind get entryKind { + EntryPoint get entryKind { if (isUncheckedEntryReference) { - return SynchronousProcedureKind.unchecked; + return EntryPoint.unchecked; } if (isCheckedEntryReference) { - return SynchronousProcedureKind.checked; + return EntryPoint.checked; } if (isBodyReference) { - return SynchronousProcedureKind.body; + return EntryPoint.body; } - return SynchronousProcedureKind.normal; + return EntryPoint.normal; } } @@ -148,7 +148,7 @@ bool _memberCanHaveMultipleEntryPoints(Member member) { return false; } -enum SynchronousProcedureKind { +enum EntryPoint { // A single procedure doing type argument checks, optional argument handling // and the body. normal, diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index 816a8ff20e6..4eab4c052fc 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -491,16 +491,19 @@ class Translator with KernelNodes { return b.emitUnreachableIfNoResult(function.type.outputs); } - void callDispatchTable(w.InstructionsBuilder b, SelectorInfo selector) { + void callDispatchTable(w.InstructionsBuilder b, SelectorInfo selector, + {required bool useUncheckedEntry}) { + final offset = selector.targets(unchecked: useUncheckedEntry).offset; + // TODO(natebiggs): Handle dispatch to dynamic module overrideable members. b.struct_get(topInfo.struct, FieldIndex.classId); - if (selector.offset! != 0) { - b.i32_const(selector.offset!); + if (offset != 0) { + b.i32_const(offset); b.i32_add(); } b.call_indirect(selector.signature, dispatchTable.getWasmTable(b.module)); b.emitUnreachableIfNoResult(selector.signature.outputs); - functions.recordSelectorUse(selector); + functions.recordSelectorUse(selector, useUncheckedEntry); } Class classForType(DartType type) { @@ -1175,18 +1178,7 @@ class Translator with KernelNodes { } w.FunctionType signatureForDirectCall(Reference target) { - if (target.asMember.isInstanceMember) { - if (target.isBodyReference) { - return makeFunctionTypeForBody(this, target.asMember); - } - if (target.isUncheckedEntryReference) { - // The unchecked entries use the same signature as normal entries. - final member = target.asMember; - return signatureForDirectCall(member is Field - ? member.setterReference! - : (member as Procedure).reference); - } - + if (target.asMember.isInstanceMember && !target.isBodyReference) { final selector = dispatchTable.selectorForTarget(target); if (selector.targetSet.contains(target)) { return selector.signature; @@ -1197,14 +1189,6 @@ class Translator with KernelNodes { ParameterInfo paramInfoForDirectCall(Reference target) { if (target.asMember.isInstanceMember) { - if (target.isUncheckedEntryReference) { - // The unchecked entries use the same signature as normal entries. - final member = target.asMember; - return paramInfoForDirectCall(member is Field - ? member.setterReference! - : (member as Procedure).reference); - } - final selector = dispatchTable.selectorForTarget(target); if (selector.targetSet.contains(target)) { return selector.paramInfo; @@ -1467,6 +1451,13 @@ class Translator with KernelNodes { bool shouldInline(Reference target, w.FunctionType signature) { if (!options.inlining) return false; + // Unchecked entry point functions perform very little, mainly optional + // parameter handling and then call the real body function. + // + // By inlining them we can often avoid downcasts and sometimes boxing. The + // force inlining here seem to even lead to overall size decreases. + if (target.isUncheckedEntryReference) return true; + final member = target.asMember; if (getPragma(member, "wasm:never-inline", true) == true) { return false; @@ -2134,14 +2125,20 @@ class PolymorphicDispatchers { final Translator translator; final w.ModuleBuilder callingModule; final cache = {}; + final uncheckedCache = {}; PolymorphicDispatchers(this.translator, this.callingModule); - CallTarget getPolymorphicDispatcher(SelectorInfo selector) { - assert(selector.targetRanges.length > 1); - return cache.putIfAbsent(selector, () { + CallTarget getPolymorphicDispatcher(SelectorInfo selector, + {required bool useUncheckedEntry}) { + assert( + selector.targets(unchecked: useUncheckedEntry).targetRanges.length > 1); + return (useUncheckedEntry && selector.useMultipleEntryPoints + ? uncheckedCache + : cache) + .putIfAbsent(selector, () { return PolymorphicDispatcherCallTarget( - translator, selector, callingModule); + translator, selector, callingModule, useUncheckedEntry); }); } } @@ -2150,9 +2147,10 @@ class PolymorphicDispatcherCallTarget extends CallTarget { final Translator translator; final SelectorInfo selector; final w.ModuleBuilder callingModule; + final bool useUncheckedEntry; - PolymorphicDispatcherCallTarget( - this.translator, this.selector, this.callingModule) + PolymorphicDispatcherCallTarget(this.translator, this.selector, + this.callingModule, this.useUncheckedEntry) : super(selector.signature); @override @@ -2162,11 +2160,16 @@ class PolymorphicDispatcherCallTarget extends CallTarget { bool get supportsInlining => true; @override - bool get shouldInline => selector.staticDispatchRanges.length <= 2; + bool get shouldInline => + selector + .targets(unchecked: useUncheckedEntry) + .staticDispatchRanges + .length <= + 2; @override - CodeGenerator get inliningCodeGen => - PolymorphicDispatcherCodeGenerator(translator, selector); + CodeGenerator get inliningCodeGen => PolymorphicDispatcherCodeGenerator( + translator, selector, useUncheckedEntry); @override late final w.BaseFunction function = (() { @@ -2182,20 +2185,24 @@ class PolymorphicDispatcherCallTarget extends CallTarget { class PolymorphicDispatcherCodeGenerator implements CodeGenerator { final Translator translator; final SelectorInfo selector; + final bool useUncheckedEntry; - PolymorphicDispatcherCodeGenerator(this.translator, this.selector); + PolymorphicDispatcherCodeGenerator( + this.translator, this.selector, this.useUncheckedEntry); @override void generate(w.InstructionsBuilder b, List paramLocals, w.Label? returnLabel) { final signature = selector.signature; - final targetRanges = selector.staticDispatchRanges + final targets = selector.targets(unchecked: useUncheckedEntry); + + final targetRanges = targets.staticDispatchRanges .map((entry) => (range: entry.range, value: entry.target)) .toList(); final bool needFallback = - selector.targetRanges.length > selector.staticDispatchRanges.length; + targets.targetRanges.length > targets.staticDispatchRanges.length; void emitDirectCall(Reference target) { for (int i = 0; i < signature.inputs.length; ++i) { @@ -2209,7 +2216,8 @@ class PolymorphicDispatcherCodeGenerator implements CodeGenerator { b.local_get(paramLocals[i]); } b.local_get(paramLocals[0]); - translator.callDispatchTable(b, selector); + translator.callDispatchTable(b, selector, + useUncheckedEntry: useUncheckedEntry); } b.local_get(paramLocals[0]);