[dart2wasm] Add support for dispatch table calls to unchecked entries
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 <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
committed by
Commit Queue
parent
7c22f942aa
commit
c833c1227c
@@ -670,11 +670,13 @@ abstract class AstCodeGenerator
|
||||
List<w.ValueType> 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;
|
||||
|
||||
@@ -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<Reference> 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<Set<w.ValueType>> outputSets = List.generate(returnCount, (_) => {});
|
||||
List<bool> 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<Reference> 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<Reference> _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 = <Row<Reference>>[];
|
||||
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<Reference> 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<Reference>(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 =
|
||||
|
||||
@@ -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<Reference, List<Range>> 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 {
|
||||
|
||||
@@ -26,6 +26,7 @@ class FunctionCollector {
|
||||
final Map<Reference, String> _exports = {};
|
||||
// Selector IDs that are invoked via GDT.
|
||||
final Set<int> _calledSelectors = {};
|
||||
final Set<int> _calledUncheckedSelectors = {};
|
||||
// Class IDs for classes that are allocated somewhere in the program
|
||||
final Set<int> _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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<bool>(member, "wasm:never-inline", true) == true) {
|
||||
return false;
|
||||
@@ -2134,14 +2125,20 @@ class PolymorphicDispatchers {
|
||||
final Translator translator;
|
||||
final w.ModuleBuilder callingModule;
|
||||
final cache = <SelectorInfo, PolymorphicDispatcherCallTarget>{};
|
||||
final uncheckedCache = <SelectorInfo, PolymorphicDispatcherCallTarget>{};
|
||||
|
||||
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<w.Local> 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]);
|
||||
|
||||
Reference in New Issue
Block a user