[dart2wasm] Reuse dispatch table entries for static calls

This has slight e main size improvements (-0.1% uncompressed, -0.2%
compressed)

Significant portion of instance calls can be devirtualized. Doing so
will make us issue direct calls to the target. But there may also be
non-devirtualized calls to the selector, so the target may be in the
dispatch table.

If we can issue a direct call to the target but the target is in a
different module we have to go via a wasm table. Currently we use an
extra wasm table for such static calls across modules.

=> If the function we want to call is already in the dispatch table we
   may call it via the existing slot instead of having to add it to
   another table as well.
=> This shrinks the size of the static wasm table.

Change-Id: Icc594b9490f2f0573190ea5a3a21b63b3ddb388a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499580
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Martin Kustermann
2026-05-01 00:02:45 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 3662682cd0
commit 95c3cab03b
5 changed files with 241 additions and 10 deletions
+17
View File
@@ -400,6 +400,10 @@ class DispatchTable {
/// class member for the selector.
late final List<Reference?> _table;
/// For direct calls across modules one can also use the existing table slots
/// in the dispatch table (instead of adding more slots to static call table).
late final Map<Reference, int> _tableIndexForReference;
late final w.TableBuilder _definedWasmTable;
late final WasmTableImporter _importedWasmTables = WasmTableImporter(
translator,
@@ -429,6 +433,13 @@ class DispatchTable {
return _selectorInfo[selectorId]!;
}
/// Returns a dispatch table index if the [target] is going to be in the
/// dispatch table.
///
/// NOTE: The [target] can occur in multiple slots in the dispatch table and
/// we return the first such index.
int? indexForTarget(Reference target) => _tableIndexForReference[target];
SelectorInfo _createSelectorForTarget(Reference target) {
Member member = target.asMember;
bool isGetter = target.isGetter || target.isTearOffReference;
@@ -756,6 +767,12 @@ class DispatchTable {
}
_table = buildRowDisplacementTable<Reference>(rows);
_tableIndexForReference = {};
for (int i = 0; i < _table.length; ++i) {
final entry = _table[i];
if (entry == null) continue;
_tableIndexForReference[entry] ??= i;
}
int rowIndex = 0;
for (final selector in selectors) {
+29 -10
View File
@@ -765,10 +765,10 @@ class Translator with KernelNodes {
outputs = b.inlineCallTo(callTarget);
} else {
b.comment('Not inlining, reason: ${decision.reason}');
outputs = callFunction(callTarget.function, b);
outputs = callFunction(callTarget.function, b, reference);
}
} else {
outputs = callFunction(callTarget.function, b);
outputs = callFunction(callTarget.function, b, reference);
}
if (callTarget.synthesizeNullReturnValue) {
assert(outputs.isEmpty);
@@ -789,18 +789,37 @@ class Translator with KernelNodes {
/// module. Otherwise does an indirect call through the static dispatch table.
List<w.ValueType> callFunction(
w.BaseFunction function,
w.InstructionsBuilder b,
) {
w.InstructionsBuilder b, [
Reference? target,
]) {
// If the target function is defined in the same module as the caller, just
// invoke it.
final targetModuleBuilder = moduleToBuilder[function.enclosingModule]!;
if (targetModuleBuilder == b.moduleBuilder) {
b.call(function);
} else {
b.i32_const(crossModuleFunctionTable.indexForFunction(function));
b.call_indirect(
function.type,
crossModuleFunctionTable.getWasmTable(b.moduleBuilder),
);
return b.emitUnreachableIfNoResult(function.type.outputs);
}
// If the target function is already available via the dispatch table, use
// it from there.
if (target != null) {
final dispatchTableIndex = dispatchTable.indexForTarget(target);
if (dispatchTableIndex != null) {
b.i32_const(dispatchTableIndex);
b.call_indirect(
function.type,
dispatchTable.getWasmTable(b.moduleBuilder),
);
return b.emitUnreachableIfNoResult(function.type.outputs);
}
}
// Otherwise add & call via the cross module function table.
b.i32_const(crossModuleFunctionTable.indexForFunction(function));
b.call_indirect(
function.type,
crossModuleFunctionTable.getWasmTable(b.moduleBuilder),
);
return b.emitUnreachableIfNoResult(function.type.outputs);
}
@@ -0,0 +1,78 @@
// Copyright (c) 2026, 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.
// functionFilter=runTest
// typeFilter=NoMatch
// globalFilter=NoMatch
// compilerOption=--enable-deferred-loading
import '' deferred as D;
void main() async {
print('main($all)');
all.forEach((x) => x.foo(always10));
await D.loadLibrary();
runTest();
}
@pragma('wasm:never-inline')
void runTest() {
final arg = always10;
for (final object in all) {
if (object is Sub1) {
object.foo(arg);
} else {
object.foo(arg);
}
}
print(D.all.toString());
}
final always10 = int.parse('10');
final all = <Base>[Base(), Sub1(), Sub2(), Sub3(), Sub4(), Sub5()];
class Base {
void foo(int arg) {
print('Base.foo($arg)');
}
}
class Sub1 extends Base {
void foo(int arg) {
print('Sub1.foo($arg)');
super.foo(arg);
}
void bar(int arg) {
print('Sub1.bar($arg)');
}
}
class Sub2 extends Base {
void foo(int arg) {
print('Sub2.foo($arg)');
super.foo(arg);
}
}
class Sub4 extends Base {
void foo(int arg) {
print('Sub4.foo($arg)');
super.foo(arg);
}
}
class Sub5 extends Base {
void foo(int arg) {
print('Sub5.foo($arg)');
super.foo(arg);
}
}
class Sub3 extends Base {
void foo(int arg) {
print('Sub3.foo($arg)');
super.foo(arg);
}
}
@@ -0,0 +1,2 @@
(module $module0
)
@@ -0,0 +1,115 @@
(module $module1
(type $#Top <...>)
(type $Array<Object?> <...>)
(type $ConcurrentModificationError <...>)
(type $JSExternWrapper <...>)
(type $Object <...>)
(type $WasmListBase <...>)
(type $_Type <...>)
(table $module0.cross-module-funcs-0 (import "module0" "cross-module-funcs-0") 12 funcref)
(table $module0.dispatch0 (import "module0" "dispatch0") 666 funcref)
(elem $module0.cross-module-funcs-0
(set 0 (ref.func $"runTest <noInline>")))
(func $"runTest <noInline>"
(local $var0 (ref $WasmListBase))
(local $var1 (ref null $#Top))
(local $var2 (ref $Object))
(local $var3 (ref $_Type))
(local $var4 i64)
(local $var5 i64)
(local $var6 i64)
i32.const 1
call_indirect $module0.cross-module-funcs-0 (result i64)
local.set $var5
i32.const 2
call_indirect $module0.cross-module-funcs-0 (result (ref $WasmListBase))
local.tee $var0
struct.get $WasmListBase $field2
local.set $var3
local.get $var0
struct.get $WasmListBase $_length
local.set $var6
loop $label0
block $label1 (result i32)
local.get $var6
local.get $var0
struct.get $WasmListBase $_length
i64.ne
if
local.get $var0
i32.const 3
call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top)) (result (ref $ConcurrentModificationError))
i32.const 4
call_indirect $module0.cross-module-funcs-0 (param (ref $#Top))
unreachable
end
local.get $var4
local.get $var6
i64.ge_s
if
ref.null none
local.set $var1
i32.const 0
br $label1
end
local.get $var0
struct.get $WasmListBase $_data
local.get $var4
i32.wrap_i64
array.get $Array<Object?>
local.set $var1
local.get $var4
i64.const 1
i64.add
local.set $var4
i32.const 1
end $label1
if
local.get $var3
struct.get $_Type $isDeclaredNullable
i32.const 1
local.get $var1
ref.is_null
select
i32.eqz
if
local.get $var1
local.get $var3
i32.const 5
call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top) (ref $_Type))
end
local.get $var1
ref.cast $Object
local.tee $var2
struct.get $Object $field0
i32.const 109
i32.eq
if
local.get $var2
local.get $var5
i32.const 487
call_indirect $module0.dispatch0 (param (ref $Object) i64)
else
local.get $var2
local.get $var5
local.get $var2
struct.get $Object $field0
i32.const 378
i32.add
call_indirect $module0.dispatch0 (param (ref $Object) i64)
end
br $label0
end
end $label0
i64.const 0
i32.const 6
call_indirect $module0.cross-module-funcs-0 (param i64) (result i32)
drop
i32.const 2
call_indirect $module0.cross-module-funcs-0 (result (ref $WasmListBase))
i32.const 7
call_indirect $module0.cross-module-funcs-0 (param (ref $Object)) (result (ref $JSExternWrapper))
i32.const 8
call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top))
)
)