From f67f4fa1722df600608d19c48e97fd107a4b4096 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 5 Mar 2026 08:15:34 +0000 Subject: [PATCH 1/2] [wasm-split] Split globals' ref.func dependencies When a global is exclusively used by a secondary module and thus moved to that module, and its initializer has a `(ref.func $func)`, we used to create a trampoline and export it from the primary module in all cases, even in the case that the function is in the same secondary module. This now moves those functions referred to by `ref.func`s to the secondary module, as long as they don't have uses anywhere else. To do this, we now skip scanning global initializers in `indirectReferencesToSecondaryFunctions`, and selectively create trampolines only when needed in `shareImportableItems`. The running time of `wasm-split` hasn't really changed with this PR, compared to the previous PR #8442 (~25s range in acx_gallery). #8441, #8442, and this PR combined reduce the size of the primary module by 46.6%. --- `wasm-objdump -h` result: - Before (#8442) ``` Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185 Import start=0x00035d48 end=0x00132efc (size=0x000fd1b4) count: 32642 Function start=0x00132f00 end=0x00145dac (size=0x00012eac) count: 62890 Table start=0x00145daf end=0x001498ea (size=0x00003b3b) count: 2921 Tag start=0x001498ec end=0x001498f0 (size=0x00000004) count: 1 Global start=0x001498f4 end=0x00289e60 (size=0x0014056c) count: 47728 Export start=0x00289e65 end=0x004977fe (size=0x0020d999) count: 35861 Start start=0x00497800 end=0x00497802 (size=0x00000002) start: 828 Elem start=0x00497806 end=0x00501649 (size=0x00069e43) count: 12303 DataCount start=0x0050164b end=0x0050164c (size=0x00000001) count: 1 Code start=0x00501651 end=0x00730f22 (size=0x0022f8d1) count: 62890 Data start=0x00730f26 end=0x00750ab3 (size=0x0001fb8d) count: 1 ``` - After (This PR) ``` Type start=0x0000000c end=0x00035d38 (size=0x00035d2c) count: 11185 Import start=0x00035d3c end=0x00132ef0 (size=0x000fd1b4) count: 32642 Function start=0x00132ef4 end=0x001436cc (size=0x000107d8) count: 53001 Table start=0x001436cf end=0x0014720a (size=0x00003b3b) count: 2921 Tag start=0x0014720c end=0x00147210 (size=0x00000004) count: 1 Global start=0x00147214 end=0x00287b75 (size=0x00140961) count: 47728 Export start=0x00287b79 end=0x002e703f (size=0x0005f4c6) count: 25972 Start start=0x002e7041 end=0x002e7043 (size=0x00000002) start: 828 Elem start=0x002e7047 end=0x00349aa7 (size=0x00062a60) count: 12303 DataCount start=0x00349aa9 end=0x00349aaa (size=0x00000001) count: 1 Code start=0x00349aaf end=0x00550a4e (size=0x00206f9f) count: 53001 Data start=0x00550a52 end=0x005705df (size=0x0001fb8d) count: 1 ``` We can see while the size of the function and the code sections have decreased, the big gains come from the decrease of the export section, which can contain long function names. --- src/ir/module-splitting.cpp | 62 +++++++++++++++++++++---- test/lit/wasm-split/global-funcref.wast | 12 +---- test/lit/wasm-split/ref.func.wast | 6 +-- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 3544610b654..ff1526359d2 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -45,7 +45,8 @@ // instantiation. // // 8. Export globals, tags, tables, and memories from the primary module and -// import them in the secondary modules. +// import them in the secondary modules. If possible, move those module +// items instead to the secondary modules. // // Functions can be used or referenced three ways in a WebAssembly module: they // can be exported, called, or referenced with ref.func. The above procedure @@ -630,7 +631,25 @@ void ModuleSplitter::indirectReferencesToSecondaryFunctions() { } } } gatherer(*this); - gatherer.walkModule(&primary); + // We shouldn't use collector.walkModuleCode here, because we don't want to + // walk on global initializers. At this point, all globals are still in the + // primary module, so if we walk on global initializers here, it will create + // unnecessary trampolines. + // + // For example, we have (global $a funcref (ref.func $foo)), and $foo was + // split into a secondary module. Because $a is at this point still in the + // primary module, $foo will be considered to exist in a different module, so + // this will create a trampoline for $foo. But it is possible that later we + // find out $a is exclusively used by that secondary module and move $a there. + // In that case, $a can just reference $foo locally, but if we scan global + // initializers here, we would have created an unnecessary trampoline for + // $foo. + walkSegments(gatherer, &primary); + for (auto& curr : primary.functions) { + if (!curr->imported()) { + gatherer.walkFunction(curr.get()); + } + } for (auto& secondaryPtr : secondaries) { gatherer.walkModule(secondaryPtr.get()); } @@ -1157,19 +1176,42 @@ void ModuleSplitter::shareImportableItems() { bool inPrimary = primaryUsed.globals.count(global->name); if (!inPrimary && usingSecondaries.size() == 1) { auto* secondary = usingSecondaries[0]; - ModuleUtils::copyGlobal(global.get(), *secondary); + auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); globalsToRemove.push_back(global->name); - // Import global initializer's ref.func dependences + + if (secondaryGlobal->init) { + // When a global's initializer contains ref.func + for (auto* ref : FindAll(secondaryGlobal->init).list) { + // If we are moving this global and its dependent function is in a + // different secondary module, we create a trampoline here. + if (allSecondaryFuncs.count(ref->func)) { + Index targetIndex = funcToSecondaryIndex.at(ref->func); + if (secondaries[targetIndex].get() != secondary) { + ref->func = getTrampoline(ref->func); + } + } + // If we are moving this global and its dependent function is in the + // primary module, we export it from there. + if (primary.getFunctionOrNull(ref->func)) { + exportImportFunction(ref->func, {secondary}); + } + // If we are moving this global and its dependent function is in the + // same secondary module, we don't need to do anything. The ref.func + // can directly reference the function. + } + } + } else { // We export / import the global if (global->init) { for (auto* ref : FindAll(global->init).list) { - // Here, ref->func is either a function the primary module, or a - // trampoline created in indirectReferencesToSecondaryFunctions in - // case the original function is in one of the secondaries. - assert(primary.getFunctionOrNull(ref->func)); - exportImportFunction(ref->func, {secondary}); + // If we are exporting this global from the primary module, we should + // create a trampoline here, because we skipped doing it for global + // initializers in indirectReferencesToSecondaryFunctions. + if (allSecondaryFuncs.count(ref->func)) { + ref->func = getTrampoline(ref->func); + } } } - } else { + for (auto* secondary : usingSecondaries) { auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); diff --git a/test/lit/wasm-split/global-funcref.wast b/test/lit/wasm-split/global-funcref.wast index 11c4a332df7..4237c3c7ecc 100644 --- a/test/lit/wasm-split/global-funcref.wast +++ b/test/lit/wasm-split/global-funcref.wast @@ -8,21 +8,11 @@ ;; TODO Use $split in the secondary module directly in the split global (module - ;; PRIMARY: (export "trampoline_split" (func $trampoline_split)) - ;; PRIMARY: (func $keep ;; PRIMARY-NEXT: ) (func $keep) - ;; PRIMARY: (func $trampoline_split - ;; PRIMARY-NEXT: (call_indirect (type $0) - ;; PRIMARY-NEXT: (i32.const 0) - ;; PRIMARY-NEXT: ) - ;; PRIMARY-NEXT: ) - - - ;; SECONDARY: (import "primary" "trampoline_split" (func $trampoline_split (exact))) - ;; SECONDARY: (global $a funcref (ref.func $trampoline_split)) + ;; SECONDARY: (global $a funcref (ref.func $split)) (global $a funcref (ref.func $split)) ;; SECONDARY: (func $split diff --git a/test/lit/wasm-split/ref.func.wast b/test/lit/wasm-split/ref.func.wast index 63dc3080b16..11007235ffe 100644 --- a/test/lit/wasm-split/ref.func.wast +++ b/test/lit/wasm-split/ref.func.wast @@ -61,7 +61,7 @@ ;; SECONDARY: (import "primary" "prime" (func $prime (exact (type $0)))) - ;; SECONDARY: (elem $0 (i32.const 0) $second $second-in-table) + ;; SECONDARY: (elem $0 (i32.const 0) $second-in-table $second) ;; SECONDARY: (elem declare func $prime) @@ -97,13 +97,13 @@ ;; (but we will get a placeholder, as all split-out functions do). ) ) -;; PRIMARY: (func $trampoline_second (type $0) +;; PRIMARY: (func $trampoline_second-in-table (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 0) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) -;; PRIMARY: (func $trampoline_second-in-table (type $0) +;; PRIMARY: (func $trampoline_second (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 1) ;; PRIMARY-NEXT: ) From a0c54d94cd774e22af635c7f5464b4d38240fcac Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 10 Mar 2026 10:11:23 +0000 Subject: [PATCH 2/2] Comment fix --- src/ir/module-splitting.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index ff1526359d2..0f35656fae6 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1175,6 +1175,7 @@ void ModuleSplitter::shareImportableItems() { getUsingSecondaries(global->name, &UsedNames::globals); bool inPrimary = primaryUsed.globals.count(global->name); if (!inPrimary && usingSecondaries.size() == 1) { + // We are moving this global to this secondary module auto* secondary = usingSecondaries[0]; auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); globalsToRemove.push_back(global->name); @@ -1182,25 +1183,28 @@ void ModuleSplitter::shareImportableItems() { if (secondaryGlobal->init) { // When a global's initializer contains ref.func for (auto* ref : FindAll(secondaryGlobal->init).list) { - // If we are moving this global and its dependent function is in a - // different secondary module, we create a trampoline here. + // If ref.func's function is in a different secondary module, we + // create a trampoline here. if (allSecondaryFuncs.count(ref->func)) { Index targetIndex = funcToSecondaryIndex.at(ref->func); if (secondaries[targetIndex].get() != secondary) { ref->func = getTrampoline(ref->func); } } - // If we are moving this global and its dependent function is in the - // primary module, we export it from there. + // 1. If ref.func's function is in the primary module, we export it + // here. + // 2. If ref.func's function is in a different secondary module and we + // just created a trampoline for it in the primary module, we + // export the trampoline here. if (primary.getFunctionOrNull(ref->func)) { exportImportFunction(ref->func, {secondary}); } - // If we are moving this global and its dependent function is in the - // same secondary module, we don't need to do anything. The ref.func - // can directly reference the function. + // If ref.func's function is in the same secondary module, we don't + // need to do anything. The ref.func can directly reference the + // function. } } - } else { // We export / import the global + } else { // We are NOT moving this global to the secondary module if (global->init) { for (auto* ref : FindAll(global->init).list) { // If we are exporting this global from the primary module, we should