From 97986a6c0ef3aaac48ccfde603c7cc7deb80fa5d Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 27 Mar 2026 06:13:53 +0000 Subject: [PATCH] [wasm-split] Pre-create trampolines for global initializers Before #8443, we scanned `ref.func`s in global initizliers early in `indirectReferencesToSecondaryFunctions` and created trampolines for them and replaced `ref.func $func`s with `ref.func $trampoline_func` if `func` was set to move to a secondary module. But in case the global containing `ref.func $trampoline_func` also ends up moving to the same secondary module, creating trampoline and using it was not necessary, because the global can simply use `ref.func $func` because `func` is in the same secondary module. To fix this, in #8443, we postponed creating trampolines for `ref.func`s in global initializers until `shareImportableItems`. This had a problem, because we end up creating new trampolines late in `shareImportableItems`. But trampolines were designed to go through `indirectCallsToSecondaryFunctions` and `setupTablePatching`, so those late trampolines were invalid, like ```wast (func $trampoline_foo (call $foo) ) ``` when `foo` is in a secondary module. This was supposed to be converted to a `call_indirect` in `indirectCallsToSecondaryFunctions` and the table elements were supposed to set up in `setupTablePatching`. --- To fix the issue, this PR pre-creates trampolines for `ref.func`s in all global initializers in `indirectReferencesToSecondaryFunctions`, before `indirectCallsToSecondaryFunctions` and `setupTablePatching`, but doesn't make `ref.func`s reference them just yet. We make `ref.func`s in global initializers reference the trampolines only when it is decided they are not moving to the same secondary module, in `shareImportableItems`. But this can leave unused trampolines in the primary module. So in `cleanupUnusedTrampolines`, we remove unused trampolines. This increases acx_gallery code size only by 0.6%. If we didn't clean up the unnecessary trampolines, the increase was 3.6%. This only removes unused trampoline functions and not placeholder imports and elements yet, but they don't affect acx_gallery because it uses `--no-placeholders`. Fixes #8510. (Both tests fail for the same reason) --- src/ir/module-splitting.cpp | 71 +++++++++++++++++++++++- test/lit/wasm-split/global-funcref.wast | 43 -------------- test/lit/wasm-split/global-reffunc.wast | 51 +++++++++++++++++ test/lit/wasm-split/global-reffunc2.wast | 53 ++++++++++++++++++ test/lit/wasm-split/ref.func.wast | 6 +- 5 files changed, 176 insertions(+), 48 deletions(-) delete mode 100644 test/lit/wasm-split/global-funcref.wast create mode 100644 test/lit/wasm-split/global-reffunc.wast create mode 100644 test/lit/wasm-split/global-reffunc2.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 834d990b383..0358c30536f 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -73,7 +73,6 @@ // from the IR before splitting. // #include "ir/module-splitting.h" -#include "ir/export-utils.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -336,6 +335,7 @@ struct ModuleSplitter { void exportImportCalledPrimaryFunctions(); void setupTablePatching(); void shareImportableItems(); + void cleanupUnusedTrampolines(); ModuleSplitter(Module& primary, const Config& config) : config(config), primary(primary), tableManager(primary), @@ -348,6 +348,7 @@ struct ModuleSplitter { exportImportCalledPrimaryFunctions(); setupTablePatching(); shareImportableItems(); + cleanupUnusedTrampolines(); } }; @@ -508,7 +509,9 @@ Name ModuleSplitter::getTrampoline(Name funcName) { primary, std::string("trampoline_") + funcName.toString()); it->second = trampoline; - // Generate the call and the function. + // Generate the call and the function. We generate a direct call here, but + // this will be converted to a call_indirect in + // indirectCallsToSecondaryFunctions. std::vector args; for (Index i = 0; i < oldFunc->getNumParams(); i++) { args.push_back(builder.makeLocalGet(i, oldFunc->getLocalType(i))); @@ -559,6 +562,21 @@ static void walkSegments(Walker& walker, Module* module) { } void ModuleSplitter::indirectReferencesToSecondaryFunctions() { + // Pre-create trampolines for secondary functions referenced in global + // initializers. We don't mutate the globals yet (that happens later in + // shareImportableItems), but we must create the trampolines now so they + // are processed correctly by indirectCallsToSecondaryFunctions and + // setupTablePatching. + for (auto& global : primary.globals) { + if (global->init) { + for (auto* ref : FindAll(global->init).list) { + if (allSecondaryFuncs.count(ref->func)) { + getTrampoline(ref->func); + } + } + } + } + // Turn references to secondary functions into references to thunks that // perform a direct call to the original referent. The direct calls in the // thunks will be handled like all other cross-module calls later, in @@ -1222,6 +1240,55 @@ void ModuleSplitter::shareImportableItems() { } } +void ModuleSplitter::cleanupUnusedTrampolines() { + // We pre-created trampolines for all functions referenced in global + // initializers' ref.funcs in indirectReferencesToSecondaryFunctions. + // But after splitting module items, if a trampoline is not used in the + // primary module, it means it was created for a global initializer but that + // global ended up moving to the same secondary module as the function + // referenced in the global initializer's ref.func. We can remove them here. + // TODO Remove placeholders and table elements created by unused trampolines + std::unordered_set usedFuncs; + + struct FuncrefCollector : public PostWalker { + std::vector& used; + FuncrefCollector(std::vector& used) : used(used) {} + void visitRefFunc(RefFunc* curr) { used.push_back(curr->func); } + }; + + ModuleUtils::ParallelFunctionAnalysis> analysis( + primary, [&](Function* func, std::vector& used) { + if (!func->imported()) { + FuncrefCollector(used).walkFunction(func); + } + }); + + for (auto& [_, usedList] : analysis.map) { + usedFuncs.insert(usedList.begin(), usedList.end()); + } + + std::vector moduleCodeUsed; + FuncrefCollector(moduleCodeUsed).walkModuleCode(&primary); + usedFuncs.insert(moduleCodeUsed.begin(), moduleCodeUsed.end()); + + for (auto& ex : primary.exports) { + if (ex->kind == ExternalKind::Function) { + usedFuncs.insert(*ex->getInternalName()); + } + } + + std::vector trampolinesToRemove; + for (auto& [_, trampoline] : trampolineMap) { + if (!usedFuncs.count(trampoline)) { + trampolinesToRemove.push_back(trampoline); + } + } + for (auto& name : trampolinesToRemove) { + primary.removeFunction(name); + primaryFuncs.erase(name); + } +} + } // anonymous namespace Results splitFunctions(Module& primary, const Config& config) { diff --git a/test/lit/wasm-split/global-funcref.wast b/test/lit/wasm-split/global-funcref.wast deleted file mode 100644 index 3faf339546c..00000000000 --- a/test/lit/wasm-split/global-funcref.wast +++ /dev/null @@ -1,43 +0,0 @@ -;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep -;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY -;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY - -;; When a split global ($a here)'s initializer contains a ref.func of a split -;; function, we should NOT create any trampolines, and the split global should -;; direclty refer to the function. - -(module - (global $a funcref (ref.func $split)) - (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (export "keep" (func $keep)) - - ;; PRIMARY-NOT: (export "trampoline_split" - ;; PRIMARY-NOT: (func $trampoline_split - - ;; SECONDARY: (import "primary" "keep" (func $keep (exact))) - - ;; SECONDARY: (global $a funcref (ref.func $split)) - ;; SECONDARY: (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (func $keep - ;; PRIMARY-NEXT: ) - (func $keep) - - ;; SECONDARY: (func $split - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $a) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $b) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: ) - (func $split - (drop - (global.get $a) - ) - (drop - (global.get $b) - ) - ) -) diff --git a/test/lit/wasm-split/global-reffunc.wast b/test/lit/wasm-split/global-reffunc.wast new file mode 100644 index 00000000000..84cd04be4d0 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc.wast @@ -0,0 +1,51 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; When a split global ($a here)'s initializer contains a ref.func of a split +;; function, we should NOT create any trampolines, and the split global should +;; direclty refer to the function. + +(module + (global $a funcref (ref.func $split)) + (global $b funcref (ref.func $keep)) + + (func $keep) + + (func $split + (drop + (global.get $a) + ) + (drop + (global.get $b) + ) + ) +) + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (table $0 1 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (export "keep" (func $keep)) +;; PRIMARY-NEXT: (func $keep +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 1 funcref)) +;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact))) +;; SECONDARY-NEXT: (global $a funcref (ref.func $split)) +;; SECONDARY-NEXT: (global $b funcref (ref.func $keep)) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split) +;; SECONDARY-NEXT: (func $split +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $a) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $b) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/global-reffunc2.wast b/test/lit/wasm-split/global-reffunc2.wast new file mode 100644 index 00000000000..9ca92fb3ae2 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc2.wast @@ -0,0 +1,53 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split1,split2 +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; Global $g1 is used (exported) in the primary module so it can't move, and +;; global $g2 is only used in the secondary module so it will move there. + +(module + (global $g1 funcref (ref.func $split1)) + (global $g2 funcref (ref.func $split2)) + (export "g1" (global $g1)) + + (func $split1 + (unreachable) + ) + + (func $split2 + (drop + (global.get $g2) + ) + ) +) + +;; PRIMARY-NEXT: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "1" (func $placeholder_1)) +;; PRIMARY-NEXT: (global $g1 funcref (ref.func $trampoline_split1)) +;; PRIMARY-NEXT: (table $0 2 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0 $placeholder_1) +;; PRIMARY-NEXT: (export "g1" (global $g1)) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (func $trampoline_split1 +;; PRIMARY-NEXT: (call_indirect (type $0) +;; PRIMARY-NEXT: (i32.const 0) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY-NEXT: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 2 funcref)) +;; SECONDARY-NEXT: (global $g2 funcref (ref.func $split2)) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split1 $split2) +;; SECONDARY-NEXT: (func $split1 +;; SECONDARY-NEXT: (unreachable) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (func $split2 +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $g2) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/ref.func.wast b/test/lit/wasm-split/ref.func.wast index 38eea498d72..5cc261dc4cd 100644 --- a/test/lit/wasm-split/ref.func.wast +++ b/test/lit/wasm-split/ref.func.wast @@ -73,7 +73,7 @@ ;; SECONDARY: (import "primary" "prime" (func $prime (exact (type $0)))) - ;; SECONDARY: (elem $0 (i32.const 0) $second-in-table $second) + ;; SECONDARY: (elem $0 (i32.const 0) $second $second-in-table) ;; SECONDARY: (elem declare func $prime) @@ -109,13 +109,13 @@ ;; (but we will get a placeholder, as all split-out functions do). ) ) -;; 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 0) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) -;; 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 1) ;; PRIMARY-NEXT: )