From 6d4b1aa024d2afd30f7dbf4105a9aa4ead83531d Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 10 Jun 2026 02:23:01 +0000 Subject: [PATCH 1/8] [wasm-split] Remove unnecessary global exports Globals and tables can have initializers that can contain other globals. Currently we just scan them as uses. For example, if global $g is used both in the primary and the secondary and its initializer is `(global.get $h)`, $h is also marked as "used" in both modules. But currently we only move a module item to a secondary module only when that item is exclusively used by that module. So if a global is used in the primary and the secondary, it will stay in the primary and then be exported to the secondary. But in the current code, becaus $g is marked as used in both modules and its initializer will be walked in both modules, $h is also marked as used in both modules. Becuase $g doesn't move to the secondary and only is imported there, the secondary doesn't need $h. But because it is marked as "used", the secondary module imports $h unnecessarily. The multi-split case is similar. The case is the same for table initiaializers. The difference between the two is global initializers can contain another global, so we need a worklist to compute the transitive closure. This fixes it by figuring out who the "owner" is for each global and table, and mark it "used" in a secondary module only when that is the sole user. Otherwise it will be marked as "used" in the primary. This does not meaningfully change computation time and reduces the primary module size around 0.3% for new acx_gallery and essentials and 1% for old acx_gallery. --- src/ir/module-splitting.cpp | 94 +++++++++++++------ test/lit/wasm-split/table-init-deps.wast | 53 +++++++++++ .../wasm-split/transitive-globals-multi.wast | 52 ++++++++++ .../transitive-globals-multi.wast.manifest | 5 + test/lit/wasm-split/transitive-globals.wast | 6 +- 5 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 test/lit/wasm-split/table-init-deps.wast create mode 100644 test/lit/wasm-split/transitive-globals-multi.wast create mode 100644 test/lit/wasm-split/transitive-globals-multi.wast.manifest diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 422b18b94ea..46ae09d4e84 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -755,13 +755,6 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } } - for (auto name : used.tables) { - if (auto* table = primary.getTableOrNull(name)) { - if (table->init) { - collector.walk(table->init); - } - } - } return used; }; @@ -821,32 +814,77 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } } - // Compute the transitive closure of globals referenced in other globals' - // initializers. Since globals can reference other globals, we must ensure - // that if a global is used in a module, all its dependencies are also marked - // as used. - auto computeTransitiveGlobals = [&](UsedNames& used) { - UniqueNonrepeatingDeferredQueue worklist; - for (auto global : used.globals) { - worklist.push(global); + // Given a name and a module item kind (field pointer), find which module + // "owns" it. If it is used by exactly one secondary module, that secondary + // module is the owner. If it is used by the primary module or multiple + // secondary modules, the primary module is the owner. If it is not used, + // returns nullptr. + auto getOwner = [&](Name name, auto UsedNames::*field) -> UsedNames* { + UsedNames* owner = nullptr; + size_t users = 0; + if ((primaryUsed.*field).contains(name)) { + owner = &primaryUsed; + users++; } - while (!worklist.empty()) { - Name name = worklist.pop(); - // At this point all globals are still in the primary module, so this - // exists - auto* global = primary.getGlobal(name); - if (!global->imported() && global->init) { - for (auto* get : FindAll(global->init).list) { - worklist.push(get->name); - used.globals.insert(get->name); - } + for (auto& sec : secondaryUsed) { + if ((sec.*field).contains(name)) { + owner = &sec; + users++; } } + if (users == 0) { + return nullptr; + } + if (users > 1) { + return &primaryUsed; + } + return owner; }; - computeTransitiveGlobals(primaryUsed); - for (auto& used : secondaryUsed) { - computeTransitiveGlobals(used); + // Scan table initializers into their owning modules. If a table is used by a + // single secondary module, its initializer dependencies belong to that + // secondary module. Otherwise, they belong to the primary module. + if (primary.features.hasGC()) { + for (auto& table : primary.tables) { + if (!table->init) { + continue; + } + UsedNames* owner = getOwner(table->name, &UsedNames::tables); + if (!owner) { + continue; + } + NameCollector(*owner).walk(table->init); + } + } + + // Compute the transitive closure of globals referenced in other globals' + // initializers. A global's initializer is evaluated by the module that + // defines it. + UniqueDeferredQueue worklist; + for (auto name : primaryUsed.globals) { + worklist.push(name); + } + for (auto& sec : secondaryUsed) { + for (auto name : sec.globals) { + worklist.push(name); + } + } + + while (!worklist.empty()) { + Name name = worklist.pop(); + auto* global = primary.getGlobal(name); + if (!global->init) { + continue; + } + UsedNames* owner = getOwner(name, &UsedNames::globals); + if (!owner) { + continue; + } + for (auto* get : FindAll(global->init).list) { + if (owner->globals.insert(get->name).second) { + worklist.push(get->name); + } + } } return std::make_pair(primaryUsed, secondaryUsed); diff --git a/test/lit/wasm-split/table-init-deps.wast b/test/lit/wasm-split/table-init-deps.wast new file mode 100644 index 00000000000..7bd40a65a1b --- /dev/null +++ b/test/lit/wasm-split/table-init-deps.wast @@ -0,0 +1,53 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; 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 + +(module + ;; PRIMARY: (type $0 (func)) + + ;; PRIMARY: (import "env" "g" (global $g funcref)) + (import "env" "g" (global $g funcref)) + + ;; A table used by both $keep and $split. + ;; The table stays in PRIMARY. $g should stay in PRIMARY and NOT be + ;; exported/imported. + ;; PRIMARY: (table $t 1 1 funcref (global.get $g)) + (table $t 1 1 funcref (global.get $g)) + + ;; PRIMARY: (export "table" (table $t)) + + ;; PRIMARY: (func $keep + ;; PRIMARY-NEXT: (drop + ;; PRIMARY-NEXT: (table.get $t + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $keep + (drop + (table.get $t + (i32.const 0) + ) + ) + ) + + ;; SECONDARY: (type $0 (func)) + + ;; SECONDARY: (import "primary" "table" (table $t 1 1 funcref)) + + ;; SECONDARY: (func $split + ;; SECONDARY-NEXT: (drop + ;; SECONDARY-NEXT: (table.get $t + ;; SECONDARY-NEXT: (i32.const 0) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: ) + (func $split + (drop + (table.get $t + (i32.const 0) + ) + ) + ) +) diff --git a/test/lit/wasm-split/transitive-globals-multi.wast b/test/lit/wasm-split/transitive-globals-multi.wast new file mode 100644 index 00000000000..f9eda9a7d59 --- /dev/null +++ b/test/lit/wasm-split/transitive-globals-multi.wast @@ -0,0 +1,52 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-split -all -g --multi-split %s --manifest %s.manifest --out-prefix=%t -o %t.wasm +;; RUN: wasm-dis %t.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t1.wasm | filecheck %s --check-prefix SECONDARY1 +;; RUN: wasm-dis %t2.wasm | filecheck %s --check-prefix SECONDARY2 + +;; Because global $e is used in both module1 ($split1) and module2 ($split2), $e +;; will be exported / imported, but we don't need to export $f. + +(module + ;; PRIMARY: (type $0 (func)) + + ;; PRIMARY: (global $f i32 (i32.const 42)) + (global $f i32 (i32.const 42)) + ;; PRIMARY: (global $e i32 (global.get $f)) + (global $e i32 (global.get $f)) + + ;; PRIMARY: (export "global" (global $e)) + + ;; PRIMARY: (func $keep + ;; PRIMARY-NEXT: (nop) + ;; PRIMARY-NEXT: ) + (func $keep + (nop) + ) + + ;; SECONDARY1: (type $0 (func)) + + ;; SECONDARY1: (import "primary" "global" (global $e i32)) + + ;; SECONDARY1: (func $split1 + ;; SECONDARY1-NEXT: (drop + ;; SECONDARY1-NEXT: (global.get $e) + ;; SECONDARY1-NEXT: ) + ;; SECONDARY1-NEXT: ) + (func $split1 + (drop (global.get $e)) + ) + + ;; SECONDARY2: (type $0 (func)) + + ;; SECONDARY2: (import "primary" "global" (global $e i32)) + + ;; SECONDARY2: (func $split2 + ;; SECONDARY2-NEXT: (drop + ;; SECONDARY2-NEXT: (global.get $e) + ;; SECONDARY2-NEXT: ) + ;; SECONDARY2-NEXT: ) + (func $split2 + (drop (global.get $e)) + ) +) diff --git a/test/lit/wasm-split/transitive-globals-multi.wast.manifest b/test/lit/wasm-split/transitive-globals-multi.wast.manifest new file mode 100644 index 00000000000..56731c0a1f9 --- /dev/null +++ b/test/lit/wasm-split/transitive-globals-multi.wast.manifest @@ -0,0 +1,5 @@ +1: +split1 + +2: +split2 diff --git a/test/lit/wasm-split/transitive-globals.wast b/test/lit/wasm-split/transitive-globals.wast index a0bac6c3364..a277ec72605 100644 --- a/test/lit/wasm-split/transitive-globals.wast +++ b/test/lit/wasm-split/transitive-globals.wast @@ -22,11 +22,9 @@ ;; PRIMARY: (global $f i32 (i32.const 42)) ;; PRIMARY: (global $e i32 (global.get $f)) - ;; PRIMARY: (export "global" (global $f)) - ;; PRIMARY: (export "global_1" (global $e)) + ;; PRIMARY: (export "global" (global $e)) - ;; SECONDARY: (import "primary" "global" (global $f i32)) - ;; SECONDARY: (import "primary" "global_1" (global $e i32)) + ;; SECONDARY: (import "primary" "global" (global $e i32)) ;; SECONDARY: (global $c i32 (i32.const 42)) ;; SECONDARY: (global $b i32 (global.get $c)) From 27041c24dd0f89d6752499ebf3dac98a151d53f6 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 11 Jun 2026 18:40:16 +0000 Subject: [PATCH 2/8] clang-format --- src/ir/module-splitting.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 46ae09d4e84..3983e57f9f1 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -819,7 +819,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { // module is the owner. If it is used by the primary module or multiple // secondary modules, the primary module is the owner. If it is not used, // returns nullptr. - auto getOwner = [&](Name name, auto UsedNames::*field) -> UsedNames* { + auto getOwner = [&](Name name, auto UsedNames::* field) -> UsedNames* { UsedNames* owner = nullptr; size_t users = 0; if ((primaryUsed.*field).contains(name)) { From 5dea2936db47445c592bf113e0253d29483f651f Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 16:03:59 -0700 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Thomas Lively --- src/ir/module-splitting.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 3983e57f9f1..d31543e125a 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -828,16 +828,13 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } for (auto& sec : secondaryUsed) { if ((sec.*field).contains(name)) { + if (owner) { + owner = &primaryUsed; + break; + } owner = &sec; - users++; } } - if (users == 0) { - return nullptr; - } - if (users > 1) { - return &primaryUsed; - } return owner; }; @@ -849,11 +846,8 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!table->init) { continue; } - UsedNames* owner = getOwner(table->name, &UsedNames::tables); - if (!owner) { - continue; - } - NameCollector(*owner).walk(table->init); + if (UsedNames* owner = getOwner(table->name, &UsedNames::tables)) { + NameCollector(*owner).walk(table->init); } } From 0957b7b2359a73139d25fc811d82c27bfdbbe569 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 23:12:31 +0000 Subject: [PATCH 4/8] Remove an unnecessary include --- src/ir/module-splitting.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index d31543e125a..7ae80b2d2d0 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -77,7 +77,6 @@ #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" -#include "support/insert_ordered.h" #include "support/unique_deferring_queue.h" #include "wasm-builder.h" #include "wasm.h" From 8558f24e1ddca4142b3058bb6fdcae0e110356c8 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 23:12:58 +0000 Subject: [PATCH 5/8] Remove remaining usage of users --- src/ir/module-splitting.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 7ae80b2d2d0..cbeff62a14b 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -820,10 +820,8 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { // returns nullptr. auto getOwner = [&](Name name, auto UsedNames::* field) -> UsedNames* { UsedNames* owner = nullptr; - size_t users = 0; if ((primaryUsed.*field).contains(name)) { owner = &primaryUsed; - users++; } for (auto& sec : secondaryUsed) { if ((sec.*field).contains(name)) { From c75f24dbf4f17d2545c9bdfed52bee94b1198fd1 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 23:13:10 +0000 Subject: [PATCH 6/8] Fix a compilation error --- src/ir/module-splitting.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index cbeff62a14b..29c996de505 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -845,6 +845,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } if (UsedNames* owner = getOwner(table->name, &UsedNames::tables)) { NameCollector(*owner).walk(table->init); + } } } From 0b3f7b468c8950293db0ac3e3365bc30bad80740 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 23:13:19 +0000 Subject: [PATCH 7/8] More use of if (UsedNames* owner = ...) --- src/ir/module-splitting.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 29c996de505..5425b982d31 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -868,13 +868,11 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!global->init) { continue; } - UsedNames* owner = getOwner(name, &UsedNames::globals); - if (!owner) { - continue; - } - for (auto* get : FindAll(global->init).list) { - if (owner->globals.insert(get->name).second) { - worklist.push(get->name); + if (UsedNames* owner = getOwner(name, &UsedNames::globals)) { + for (auto* get : FindAll(global->init).list) { + if (owner->globals.insert(get->name).second) { + worklist.push(get->name); + } } } } From b490f2a97d7eb5cb89b34355708f4020b887be60 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 12 Jun 2026 23:49:04 +0000 Subject: [PATCH 8/8] Improve comments --- src/ir/module-splitting.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 5425b982d31..b557e774855 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -836,8 +836,9 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { }; // Scan table initializers into their owning modules. If a table is used by a - // single secondary module, its initializer dependencies belong to that - // secondary module. Otherwise, they belong to the primary module. + // single secondary module, its initializer dependencies are marked as "used" + // in that secondary module. Otherwise, they are marked as used in the primary + // module. if (primary.features.hasGC()) { for (auto& table : primary.tables) { if (!table->init) {