From 17cd994d6e46f7d28ec90d68fb91815770538565 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sat, 29 Aug 2026 08:05:59 +0000 Subject: [PATCH 1/6] [wasm-split] Split immutable globals Previously, we treated all modules items in the same way: if a module item was used in the primary module or multiple secondary modules, we put the item in the primary module and exported it from there, and secondary modules using the item imported it. So 1. When an item was used in both the primary and secondary modules, it was placed in the primary module and exported from there, and imported from the secondary modules 2. When an item was not used in the primary module but used in multiple secondary modules, it was still placed in the primary module and exported from there, and imported from the secondary modules But in case of immutable globals, we can have as many copies of the same global as possible, and we can just copy them to the secondary modules. So whether it is used in the primary module or multiple secondary modules, each module that uses it has its own copy. This reduces the primary module size because, in case of 1, we don't need to put the global in the primary module in the first place. Also in case of 2, we don't need to add an export of a global to the primary module. This is a trade-off and may increase the secondary module sizes instead. But given that the main goal of using wasm-split is mostly to reduce the loading time of the primary module, I think this trade-off is worth it. In case of merge-opt-split workflow, after interprocedural optimizations, many globals that used to be only used in a single secondary module gets used in multiple secondary modules, and thus are placed in the primary module after splitting, increasing the primary module size. This undoes this effect. Note that this also copy immutable global imports to secondary modules whenever it is possible, because we didn't exclude them in `shareElement`. --- Results - acx_gallery - Input - Total: 23648k - Primary: 1337k - Current - Total: 22240k - Primary: 1579k - This PR - Total: 22884k - vs. Current: +2.8% - vs. Input: -3.2% - Primary: 1348k - vs. Current: -14.6% - vs. Input: +0.8% - essentials - Input - Total: 83384k - Primary: 5600k - Current - Total: 77060k - Primary: 6396k - This PR - Total: 79624k - vs. Current: +3.3% - vs. Input: -4.5% - Primary: 5715k - vs. Current: -10.6% - vs. Input: +2.0% Compared to the current code (without this patch), we can see this reduces the primary module size significantly (-14.6% and -10.6% each) at the expense of slight increase of the total combined module size (+3-4%). But compared to the original input, this still does not reduce the primary module size (+1-2%), while it decreases the combined module size slightly (-3-4%). The measurements were done with with 07/2026 version of Dart apps, and after removing all (internal) exports that were not marked as `@binaryen.js.called`. (I have a pass that does this in my local machine, but I haven't uploaded it yet) --- src/ir/module-splitting.cpp | 93 +++++++++++++++++-- ...atch-table-base-global-used-elsewhere.wast | 4 +- test/lit/wasm-split/passive-deps.wast | 3 +- test/lit/wasm-split/split-module-items.wast | 17 ++-- test/lit/wasm-split/transitive-globals.wast | 16 ++-- test/lit/wasm-split/transitive-globals2.wast | 9 +- .../transitive-immutable-globals-multi.wast | 14 +-- 7 files changed, 115 insertions(+), 41 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index b340ad5f874..f4ed8e61fbe 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -336,6 +336,7 @@ struct OwnershipTracker { struct ItemInfo { UsedNames* owner = nullptr; SmallVector usingSecondaries; + bool usedByPrimary = false; }; std::unordered_map tables; @@ -376,6 +377,9 @@ struct OwnershipTracker { // used by the primary module or multiple secondary modules, the primary // module is the owner. auto [it, inserted] = (this->*mapField).insert({name, ItemInfo{owner, {}}}); + if (owner == &primaryUsed) { + it->second.usedByPrimary = true; + } Module* secondary = nullptr; if (owner != &primaryUsed) { size_t index = owner - secondaryUsed.data(); @@ -420,13 +424,29 @@ struct OwnershipTracker { } bool isUnused(Name name, const std::unordered_map& map) { - return getOwner(name, map) == nullptr; + auto it = map.find(name); + if (it != map.end()) { + return !it->second.usedByPrimary && it->second.usingSecondaries.empty(); + } + return true; + } + + bool isUsedByPrimary(Name name, + const std::unordered_map& map) { + auto it = map.find(name); + if (it != map.end()) { + return it->second.usedByPrimary; + } + return false; } bool usedBySingleSecondary(Name name, const std::unordered_map& map) { - auto* owner = getOwner(name, map); - return owner != nullptr && owner != &primaryUsed; + auto it = map.find(name); + if (it != map.end()) { + return !it->second.usedByPrimary && it->second.usingSecondaries.size() == 1; + } + return false; } }; @@ -1047,9 +1067,39 @@ void ModuleSplitter::computeUsedNames() { if (!global->init) { continue; } - if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { - for (auto* get : FindAll(global->init).list) { - tracker.insert(get->name, owner); + auto gets = FindAll(global->init).list; + if (gets.empty()) { + continue; + } + + // In case of mutable globals, we cannot have multiple copies. Compute the + // 'owner' of the global and insert its dependent globals there. + if (global->mutable_) { + if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { + for (auto* get : gets) { + tracker.insert(get->name, owner); + } + } + continue; + } + + // In case of immutable globals, we can have multiple copies of it. To + // reduce the primary module size, we just copy the globals to all secondary + // modules using them. So here we insert the global in all using modules. + if (tracker.isUsedByPrimary(global->name, tracker.globals)) { + for (auto* get : gets) { + tracker.insert(get->name, &primaryUsed); + } + } + auto& usingSecs = + tracker.getUsingSecondaries(global->name, tracker.globals); + for (size_t i = 0; i < secondaries.size(); ++i) { + Module* sec = secondaries[i].get(); + if (std::find(usingSecs.begin(), usingSecs.end(), sec) != + usingSecs.end()) { + for (auto* get : gets) { + tracker.insert(get->name, &secondaryUsed[i]); + } } } } @@ -1063,8 +1113,8 @@ void ModuleSplitter::shareImportableItems() { // 2. If an item is used by only a single secondary module, move the item to // that secondary module. If an item is used by multiple modules (including // the primary and secondary modules), export the item from the primary and - // import it from the using secondary modules. - + // import it from the using secondary modules. (except for immutable + // globals, which we just copy to using secondary modules) auto shareElements = [&](auto& elements, auto& trackerElements, auto copyElement, @@ -1073,18 +1123,43 @@ void ModuleSplitter::shareImportableItems() { ExternalKind kind = ExternalKind::Invalid) { std::unordered_set elementsToRemove; for (auto& element : elements) { + using T = std::remove_pointer_t; + bool isImmutableGlobal = false; + if constexpr (std::is_same_v) { + if (!element->mutable_) { + isImmutableGlobal = true; + } + } + if (tracker.isUnused(element->name, trackerElements)) { + // If this element is not used anywhere, just remove it elementsToRemove.insert(element->name); } else if (tracker.usedBySingleSecondary(element->name, trackerElements)) { + // If this element is used in a single secondary module, move it to the + // secondary module auto* secondary = tracker.getUsingSecondaries(element->name, trackerElements)[0]; copyElement(element.get(), *secondary); elementsToRemove.insert(element->name); + } else if (isImmutableGlobal) { + // If this element is an immutable global, copy it to all using + // secondary modules to reduce the primary module size, because we can + // have multiple copies. If it is not used in the primary module, remove + // it from there. + if (!tracker.isUsedByPrimary(element->name, trackerElements)) { + elementsToRemove.insert(element->name); + } + for (auto* secondary : + tracker.getUsingSecondaries(element->name, trackerElements)) { + copyElement(element.get(), *secondary); + } } else { + // If this element is used by multiple modules (primary + multiple + // secondaries or just multiple secondaries), export them from the + // primary module and import them from using secondary modules. // We only import and export Importables, i.e., we don't do this for // segments. - using T = std::remove_pointer_t; if constexpr (std::is_base_of_v) { for (auto* secondary : tracker.getUsingSecondaries(element->name, trackerElements)) { diff --git a/test/lit/wasm-split/dispatch-table-base-global-used-elsewhere.wast b/test/lit/wasm-split/dispatch-table-base-global-used-elsewhere.wast index b860fb144a5..a19bf77b939 100644 --- a/test/lit/wasm-split/dispatch-table-base-global-used-elsewhere.wast +++ b/test/lit/wasm-split/dispatch-table-base-global-used-elsewhere.wast @@ -25,8 +25,6 @@ ;; PRIMARY: (export "table" (table $table)) - ;; PRIMARY: (export "global" (global $base)) - ;; PRIMARY: (export "keep" (func $keep)) ;; PRIMARY: (func $keep @@ -42,7 +40,7 @@ ) ;; SECONDARY: (import "primary" "table" (table $table 2 2 funcref)) - ;; SECONDARY: (import "primary" "global" (global $base i32)) + ;; SECONDARY: (import "env" "base" (global $base i32)) ;; SECONDARY: (import "primary" "keep" (func $keep)) diff --git a/test/lit/wasm-split/passive-deps.wast b/test/lit/wasm-split/passive-deps.wast index 9c14bb09e98..5f16ebc596c 100644 --- a/test/lit/wasm-split/passive-deps.wast +++ b/test/lit/wasm-split/passive-deps.wast @@ -22,7 +22,6 @@ ;; PRIMARY-NEXT: (type $0 (func)) ;; PRIMARY-NEXT: (global $g funcref (ref.null nofunc)) ;; PRIMARY-NEXT: (elem $passive-elem funcref (item (global.get $g))) -;; PRIMARY-NEXT: (export "global" (global $g)) ;; PRIMARY-NEXT: (func $keep (type $0) ;; PRIMARY-NEXT: (elem.drop $passive-elem) ;; PRIMARY-NEXT: ) @@ -30,7 +29,7 @@ ;; SECONDARY: (module ;; SECONDARY-NEXT: (type $0 (func)) -;; SECONDARY-NEXT: (import "primary" "global" (global $g funcref)) +;; SECONDARY-NEXT: (global $g funcref (ref.null nofunc)) ;; SECONDARY-NEXT: (func $split (type $0) ;; SECONDARY-NEXT: (drop ;; SECONDARY-NEXT: (global.get $g) diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index b4a8b3db881..eb5f59b962c 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -5,8 +5,10 @@ ;; Check that ;; 1. Items only used in the primary module stay in the primary module ;; 2. Items only used in the secondary module are moved to the secondary module -;; 3. Items used in both modules are exported from the primary and imported from -;; the secondary module +;; 3. Items (that are not immutable globals) used in both modules are exported +;; from the primary and imported from the secondary module +;; 3-1. Immutable globals used in both modules stay in the primary module and +;; also are copied to the secondary module (module (rec @@ -78,24 +80,23 @@ ;; PRIMARY-NEXT: (export "memory_1" (memory $shared-memory)) ;; PRIMARY-NEXT: (export "table" (table $keep-table2)) ;; PRIMARY-NEXT: (export "table_3" (table $shared-table)) - ;; PRIMARY-NEXT: (export "global" (global $shared-immutable-global)) - ;; PRIMARY-NEXT: (export "global_5" (global $shared-mutable-global)) + ;; PRIMARY-NEXT: (export "global" (global $shared-mutable-global)) ;; PRIMARY-NEXT: (export "tag" (tag $shared-tag)) ;; PRIMARY-NEXT: (export "keep" (func $keep)) - ;; PRIMARY-NEXT: (export "table_8" (table $3)) + ;; PRIMARY-NEXT: (export "table_7" (table $3)) ;; SECONDARY: (import "primary" "memory" (memory $keep-memory2 1 1)) ;; SECONDARY-NEXT: (import "primary" "memory_1" (memory $shared-memory 1 1)) ;; SECONDARY-NEXT: (import "primary" "table" (table $keep-table2 1 1 (ref null $2))) ;; SECONDARY-NEXT: (import "primary" "table_3" (table $shared-table 1 1 funcref)) - ;; SECONDARY-NEXT: (import "primary" "table_8" (table $timport$2 1 funcref)) - ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-immutable-global i32)) - ;; SECONDARY-NEXT: (import "primary" "global_5" (global $shared-mutable-global (mut i32))) + ;; SECONDARY-NEXT: (import "primary" "table_7" (table $timport$2 1 funcref)) + ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-mutable-global (mut i32))) ;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact (param i32) (result i32)))) ;; SECONDARY-NEXT: (import "primary" "tag" (tag $shared-tag (type $1) (param i32))) ;; SECONDARY: (global $split-immutable-global i32 (i32.const 20)) ;; SECONDARY-NEXT: (global $split-mutable-global (mut i32) (i32.const 20)) + ;; SECONDARY-NEXT: (global $shared-immutable-global i32 (i32.const 20)) ;; SECONDARY-NEXT: (memory $split-memory 1 1) ;; SECONDARY-NEXT: (data $split-data (memory $split-memory) (i32.const 0) "a") ;; SECONDARY-NEXT: (table $split-table 1 1 funcref) diff --git a/test/lit/wasm-split/transitive-globals.wast b/test/lit/wasm-split/transitive-globals.wast index ad702a4363b..00fc2d3bd3e 100644 --- a/test/lit/wasm-split/transitive-globals.wast +++ b/test/lit/wasm-split/transitive-globals.wast @@ -3,13 +3,14 @@ ;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY ;; Check that transitive dependencies in global initializers are correctly -;; analyzed and moved to the secondary module. +;; analyzed and copied to the secondary module. (module - ;; There are two dependency chains: $a->$b->$c and $d->$e->$f. While all of - ;; $a, $b, and $c can be moved to the secondary module because all f them are - ;; used only there, $e is used in the primary module, preventing $e and $f - ;; from being moved to the secondary module. + ;; There are two dependency chains: $a->$b->$c and $d->$e->$f. Because these + ;; are immutable globals, globals are copied to whichever module they are + ;; used. The secondary module uses $a and $d, so it will have all globals + ;; copied to it. The primary module only uses $e, so it will have $e and its + ;; dependency $f. (global $c i32 (i32.const 42)) (global $b i32 (global.get $c)) @@ -29,7 +30,6 @@ ) ) - ;; Exclusively uses $a and $d, causing them to move to the secondary module (func $split (drop (global.get $a) @@ -44,7 +44,6 @@ ;; PRIMARY-NEXT: (type $0 (func)) ;; PRIMARY-NEXT: (global $f i32 (i32.const 42)) ;; PRIMARY-NEXT: (global $e i32 (global.get $f)) -;; PRIMARY-NEXT: (export "global" (global $e)) ;; PRIMARY-NEXT: (func $keep ;; PRIMARY-NEXT: (drop ;; PRIMARY-NEXT: (global.get $e) @@ -54,10 +53,11 @@ ;; SECONDARY: (module ;; SECONDARY-NEXT: (type $0 (func)) -;; SECONDARY-NEXT: (import "primary" "global" (global $e i32)) ;; SECONDARY-NEXT: (global $c i32 (i32.const 42)) ;; SECONDARY-NEXT: (global $b i32 (global.get $c)) ;; SECONDARY-NEXT: (global $a i32 (global.get $b)) +;; SECONDARY-NEXT: (global $f i32 (i32.const 42)) +;; SECONDARY-NEXT: (global $e i32 (global.get $f)) ;; SECONDARY-NEXT: (global $d i32 (global.get $e)) ;; SECONDARY-NEXT: (func $split ;; SECONDARY-NEXT: (drop diff --git a/test/lit/wasm-split/transitive-globals2.wast b/test/lit/wasm-split/transitive-globals2.wast index 56e96807aae..04944971521 100644 --- a/test/lit/wasm-split/transitive-globals2.wast +++ b/test/lit/wasm-split/transitive-globals2.wast @@ -3,8 +3,9 @@ ;; RUN: wasm-dis -all %t.2.wasm | filecheck %s --check-prefix SECONDARY ;; The dependence chain is $g4->$g3->$g2->$g1, and because $g4 is used in the -;; primary module, all four globals should end up in the primary module. Only -;; $g2 needs to be exported to the secondary module, not $g1. +;; primary module, all four globals should end up in the primary module. The +;; secondary module uses $g2, so it will have $g2 and $g1 it depends on. +;; (Immmutable globals are copied to whichever modules they are used.) (module (global $g1 i32 (i32.const 42)) @@ -27,7 +28,6 @@ ;; PRIMARY-NEXT: (global $g2 i32 (global.get $g1)) ;; PRIMARY-NEXT: (global $g3 i32 (global.get $g2)) ;; PRIMARY-NEXT: (global $g4 i32 (global.get $g3)) -;; PRIMARY-NEXT: (export "global" (global $g2)) ;; PRIMARY-NEXT: (func $keep (type $0) ;; PRIMARY-NEXT: (drop ;; PRIMARY-NEXT: (global.get $g4) @@ -37,7 +37,8 @@ ;; SECONDARY: (module ;; SECONDARY-NEXT: (type $0 (func)) -;; SECONDARY-NEXT: (import "primary" "global" (global $g2 i32)) +;; SECONDARY-NEXT: (global $g1 i32 (i32.const 42)) +;; SECONDARY-NEXT: (global $g2 i32 (global.get $g1)) ;; SECONDARY-NEXT: (func $split (type $0) ;; SECONDARY-NEXT: (drop ;; SECONDARY-NEXT: (global.get $g2) diff --git a/test/lit/wasm-split/transitive-immutable-globals-multi.wast b/test/lit/wasm-split/transitive-immutable-globals-multi.wast index 3f2cb97c392..72949f83972 100644 --- a/test/lit/wasm-split/transitive-immutable-globals-multi.wast +++ b/test/lit/wasm-split/transitive-immutable-globals-multi.wast @@ -3,8 +3,9 @@ ;; RUN: wasm-dis -all %t1.wasm | filecheck %s --check-prefix SECONDARY1 ;; RUN: wasm-dis -all %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. +;; In case of immutable globals, they are copied to whichever modules they are +;; used. $e will be copied to both module1 ($split1) and module2 ($split2), and +;; this will also cause $f to be copied to both modules. (module (global $f i32 (i32.const 42)) @@ -25,9 +26,6 @@ ;; PRIMARY: (module ;; PRIMARY-NEXT: (type $0 (func)) -;; PRIMARY-NEXT: (global $f i32 (i32.const 42)) -;; PRIMARY-NEXT: (global $e i32 (global.get $f)) -;; PRIMARY-NEXT: (export "global" (global $e)) ;; PRIMARY-NEXT: (func $keep (type $0) ;; PRIMARY-NEXT: (nop) ;; PRIMARY-NEXT: ) @@ -35,7 +33,8 @@ ;; SECONDARY1: (module ;; SECONDARY1-NEXT: (type $0 (func)) -;; SECONDARY1-NEXT: (import "primary" "global" (global $e i32)) +;; SECONDARY1-NEXT: (global $f i32 (i32.const 42)) +;; SECONDARY1-NEXT: (global $e i32 (global.get $f)) ;; SECONDARY1-NEXT: (func $split1 (type $0) ;; SECONDARY1-NEXT: (drop ;; SECONDARY1-NEXT: (global.get $e) @@ -45,7 +44,8 @@ ;; SECONDARY2: (module ;; SECONDARY2-NEXT: (type $0 (func)) -;; SECONDARY2-NEXT: (import "primary" "global" (global $e i32)) +;; SECONDARY2-NEXT: (global $f i32 (i32.const 42)) +;; SECONDARY2-NEXT: (global $e i32 (global.get $f)) ;; SECONDARY2-NEXT: (func $split2 (type $0) ;; SECONDARY2-NEXT: (drop ;; SECONDARY2-NEXT: (global.get $e) From b40df2448d88ec18882f90a5f23f0d13d2f66bf9 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 1 Sep 2026 01:07:14 +0000 Subject: [PATCH 2/6] clang-format --- src/ir/module-splitting.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index f4ed8e61fbe..f6ff1ad08d5 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -444,7 +444,8 @@ struct OwnershipTracker { const std::unordered_map& map) { auto it = map.find(name); if (it != map.end()) { - return !it->second.usedByPrimary && it->second.usingSecondaries.size() == 1; + return !it->second.usedByPrimary && + it->second.usingSecondaries.size() == 1; } return false; } From 9a0b05dd8ebb75e754a5933abb944f9fda1213c5 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 9 Nov 2026 07:18:03 +0000 Subject: [PATCH 3/6] Don't copy generative immutable globals --- src/ir/module-splitting.cpp | 40 ++++++++----- test/lit/wasm-split/split-module-items.wast | 57 +++++++++++++------ .../transitive-immutable-globals-multi.wast | 44 +++++++++++--- 3 files changed, 101 insertions(+), 40 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index a859e1d7048..263b30e4c8d 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -77,6 +77,7 @@ #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" +#include "ir/properties.h" #include "support/small_vector.h" #include "support/stdckdint.h" #include "wasm-builder.h" @@ -779,6 +780,17 @@ void ModuleSplitter::thunkExportedSecondaryFunctions() { } } +bool canCopyGlobal(Global* global, Module& wasm) { + // Mutable globals cannot be copied to multiple modules because mutations in + // one module would not be visible in others. Immutable globals with + // generative initializers (e.g. struct.new) also cannot be copied because + // there would be an observable difference (e.g. struct identity tested with + // ref.eq) between the values in the copies. + return !global->mutable_ && + (!global->init || + !Properties::isGenerative(global->init, nullptr, wasm)); +} + void ModuleSplitter::computeUsedNames() { tracker.secondaries = &secondaries; tracker.secondaryUsed.resize(secondaries.size()); @@ -1073,9 +1085,10 @@ void ModuleSplitter::computeUsedNames() { continue; } - // In case of mutable globals, we cannot have multiple copies. Compute the - // 'owner' of the global and insert its dependent globals there. - if (global->mutable_) { + // In case of mutable globals or immutable globals with generative + // initializers (e.g. struct.new), we cannot have multiple copies. Compute + // the 'owner' of the global and insert its dependent globals there. + if (!canCopyGlobal(global.get(), primary)) { if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { for (auto* get : gets) { tracker.insert(get->name, owner); @@ -1084,9 +1097,10 @@ void ModuleSplitter::computeUsedNames() { continue; } - // In case of immutable globals, we can have multiple copies of it. To - // reduce the primary module size, we just copy the globals to all secondary - // modules using them. So here we insert the global in all using modules. + // In case of copyable immutable globals, we can have multiple copies of + // them. To reduce the primary module size, we just copy the globals to all + // secondary modules using them. So here we insert the dependent globals in + // all using modules. if (tracker.isUsedByPrimary(global->name, tracker.globals)) { for (auto* get : gets) { tracker.insert(get->name, &primaryUsed); @@ -1114,8 +1128,8 @@ void ModuleSplitter::shareImportableItems() { // 2. If an item is used by only a single secondary module, move the item to // that secondary module. If an item is used by multiple modules (including // the primary and secondary modules), export the item from the primary and - // import it from the using secondary modules. (except for immutable - // globals, which we just copy to using secondary modules) + // import it from the using secondary modules. (except for non-generative + // immutable globals, which we just copy to using secondary modules) auto shareElements = [&](auto& elements, auto& trackerElements, auto copyElement, @@ -1125,11 +1139,9 @@ void ModuleSplitter::shareImportableItems() { std::unordered_set elementsToRemove; for (auto& element : elements) { using T = std::remove_pointer_t; - bool isImmutableGlobal = false; + bool isCopyableGlobal = false; if constexpr (std::is_same_v) { - if (!element->mutable_) { - isImmutableGlobal = true; - } + isCopyableGlobal = canCopyGlobal(element.get(), primary); } if (tracker.isUnused(element->name, trackerElements)) { @@ -1143,8 +1155,8 @@ void ModuleSplitter::shareImportableItems() { tracker.getUsingSecondaries(element->name, trackerElements)[0]; copyElement(element.get(), *secondary); elementsToRemove.insert(element->name); - } else if (isImmutableGlobal) { - // If this element is an immutable global, copy it to all using + } else if (isCopyableGlobal) { + // If this element is a copyable immutable global, copy it to all using // secondary modules to reduce the primary module size, because we can // have multiple copies. If it is not used in the primary module, remove // it from there. diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index eb5f59b962c..c475367d1c8 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -5,16 +5,19 @@ ;; Check that ;; 1. Items only used in the primary module stay in the primary module ;; 2. Items only used in the secondary module are moved to the secondary module -;; 3. Items (that are not immutable globals) used in both modules are exported -;; from the primary and imported from the secondary module -;; 3-1. Immutable globals used in both modules stay in the primary module and -;; also are copied to the secondary module +;; 3. Items (that are not copyable immutable globals) used in both modules are +;; exported from the primary and imported from the secondary module +;; 3-1. Non-generative immutable globals used in both modules stay in the +;; primary module and also are copied to the secondary module +;; 3-2. Generative immutable globals (e.g. struct.new) used in both modules are +;; exported from the primary and imported from the secondary module (module (rec (type $struct (descriptor $desc) (struct)) (type $desc (describes $struct) (struct)) ) + (type $simple-struct (struct)) (global $null-desc (ref null none) (ref.null none)) (memory $keep-memory 1 1) @@ -22,6 +25,7 @@ ;; so it is pinned to the primary, so this will be too. (memory $keep-memory2 1 1) (global $keep-immutable-global i32 (i32.const 20)) + (global $keep-immutable-generative-global (ref $simple-struct) (struct.new_default $simple-struct)) (global $keep-mutable-global (mut i32) (i32.const 20)) (table $keep-table 1 1 funcref) ;; This is only used in the secondary module, but segments $keep-elem2 and @@ -39,6 +43,7 @@ (memory $split-memory 1 1) (global $split-immutable-global i32 (i32.const 20)) + (global $split-immutable-generative-global (ref $simple-struct) (struct.new_default $simple-struct)) (global $split-mutable-global (mut i32) (i32.const 20)) (table $split-table 1 1 funcref) (tag $split-tag (param i32)) @@ -47,6 +52,7 @@ (memory $shared-memory 1 1) (global $shared-immutable-global i32 (i32.const 20)) + (global $shared-immutable-generative-global (ref $simple-struct) (struct.new_default $simple-struct)) (global $shared-mutable-global (mut i32) (i32.const 20)) (table $shared-table 1 1 funcref) (tag $shared-tag (param i32)) @@ -54,8 +60,10 @@ (data $shared-data (memory $shared-memory) (i32.const 0) "a") ;; PRIMARY: (global $keep-immutable-global i32 (i32.const 20)) + ;; PRIMARY-NEXT: (global $keep-immutable-generative-global (ref $simple-struct) (struct.new_default $simple-struct)) ;; PRIMARY-NEXT: (global $keep-mutable-global (mut i32) (i32.const 20)) ;; PRIMARY-NEXT: (global $shared-immutable-global i32 (i32.const 20)) + ;; PRIMARY-NEXT: (global $shared-immutable-generative-global (ref $simple-struct) (struct.new_default $simple-struct)) ;; PRIMARY-NEXT: (global $shared-mutable-global (mut i32) (i32.const 20)) ;; PRIMARY-NEXT: (memory $keep-memory 1 1) ;; PRIMARY-NEXT: (memory $keep-memory2 1 1) @@ -73,35 +81,38 @@ ;; PRIMARY-NEXT: (global.get $null-desc) ;; PRIMARY-NEXT: ))) ;; PRIMARY-NEXT: (elem $shared-elem (table $shared-table) (i32.const 0) funcref (item (ref.null nofunc))) - ;; PRIMARY: (tag $keep-tag (type $3) (param i32)) - ;; PRIMARY-NEXT: (tag $shared-tag (type $3) (param i32)) + ;; PRIMARY: (tag $keep-tag (type $4) (param i32)) + ;; PRIMARY-NEXT: (tag $shared-tag (type $4) (param i32)) ;; PRIMARY: (export "memory" (memory $keep-memory2)) ;; PRIMARY-NEXT: (export "memory_1" (memory $shared-memory)) ;; PRIMARY-NEXT: (export "table" (table $keep-table2)) ;; PRIMARY-NEXT: (export "table_3" (table $shared-table)) - ;; PRIMARY-NEXT: (export "global" (global $shared-mutable-global)) + ;; PRIMARY-NEXT: (export "global" (global $shared-immutable-generative-global)) + ;; PRIMARY-NEXT: (export "global_5" (global $shared-mutable-global)) ;; PRIMARY-NEXT: (export "tag" (tag $shared-tag)) ;; PRIMARY-NEXT: (export "keep" (func $keep)) - ;; PRIMARY-NEXT: (export "table_7" (table $3)) + ;; PRIMARY-NEXT: (export "table_8" (table $3)) ;; SECONDARY: (import "primary" "memory" (memory $keep-memory2 1 1)) ;; SECONDARY-NEXT: (import "primary" "memory_1" (memory $shared-memory 1 1)) - ;; SECONDARY-NEXT: (import "primary" "table" (table $keep-table2 1 1 (ref null $2))) + ;; SECONDARY-NEXT: (import "primary" "table" (table $keep-table2 1 1 (ref null $3))) ;; SECONDARY-NEXT: (import "primary" "table_3" (table $shared-table 1 1 funcref)) - ;; SECONDARY-NEXT: (import "primary" "table_7" (table $timport$2 1 funcref)) - ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-mutable-global (mut i32))) + ;; SECONDARY-NEXT: (import "primary" "table_8" (table $timport$2 1 funcref)) + ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-immutable-generative-global (ref $0))) + ;; SECONDARY-NEXT: (import "primary" "global_5" (global $shared-mutable-global (mut i32))) ;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact (param i32) (result i32)))) - ;; SECONDARY-NEXT: (import "primary" "tag" (tag $shared-tag (type $1) (param i32))) + ;; SECONDARY-NEXT: (import "primary" "tag" (tag $shared-tag (type $2) (param i32))) ;; SECONDARY: (global $split-immutable-global i32 (i32.const 20)) + ;; SECONDARY-NEXT: (global $split-immutable-generative-global (ref $0) (struct.new_default $0)) ;; SECONDARY-NEXT: (global $split-mutable-global (mut i32) (i32.const 20)) ;; SECONDARY-NEXT: (global $shared-immutable-global i32 (i32.const 20)) ;; SECONDARY-NEXT: (memory $split-memory 1 1) ;; SECONDARY-NEXT: (data $split-data (memory $split-memory) (i32.const 0) "a") ;; SECONDARY-NEXT: (table $split-table 1 1 funcref) ;; SECONDARY-NEXT: (elem $split-elem (table $split-table) (i32.const 0) funcref (item (ref.null nofunc))) - ;; SECONDARY: (tag $split-tag (type $1) (param i32)) + ;; SECONDARY: (tag $split-tag (type $2) (param i32)) (func $keep (param i32) (result i32) (call $split (i32.const 0)) @@ -118,10 +129,13 @@ (i32.const 0) ) ) - ;; Uses $keep-immutable-global and $keep-mutable-global + ;; Uses $keep-immutable-global, $keep-immutable-generative-global, and $keep-mutable-global (drop (global.get $keep-immutable-global) ) + (drop + (global.get $keep-immutable-generative-global) + ) (drop (global.get $keep-mutable-global) ) @@ -142,10 +156,13 @@ (i32.const 0) ) ) - ;; Uses $shared-immutable-global and $shared-mutable-global + ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and $shared-mutable-global (drop (global.get $shared-immutable-global) ) + (drop + (global.get $shared-immutable-generative-global) + ) (drop (global.get $shared-mutable-global) ) @@ -181,10 +198,13 @@ (drop (table.get $keep-table2 (i32.const 0)) ) - ;; Uses $split-immutable-global and $shared-mutable-global + ;; Uses $split-immutable-global, $split-immutable-generative-global, and $split-mutable-global (drop (global.get $split-immutable-global) ) + (drop + (global.get $split-immutable-generative-global) + ) (drop (global.get $split-mutable-global) ) @@ -205,10 +225,13 @@ (i32.const 0) ) ) - ;; Uses $shared-immutable-global and $shared-mutable-global + ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and $shared-mutable-global (drop (global.get $shared-immutable-global) ) + (drop + (global.get $shared-immutable-generative-global) + ) (drop (global.get $shared-mutable-global) ) diff --git a/test/lit/wasm-split/transitive-immutable-globals-multi.wast b/test/lit/wasm-split/transitive-immutable-globals-multi.wast index 72949f83972..3825f08dff2 100644 --- a/test/lit/wasm-split/transitive-immutable-globals-multi.wast +++ b/test/lit/wasm-split/transitive-immutable-globals-multi.wast @@ -3,52 +3,78 @@ ;; RUN: wasm-dis -all %t1.wasm | filecheck %s --check-prefix SECONDARY1 ;; RUN: wasm-dis -all %t2.wasm | filecheck %s --check-prefix SECONDARY2 -;; In case of immutable globals, they are copied to whichever modules they are -;; used. $e will be copied to both module1 ($split1) and module2 ($split2), and -;; this will also cause $f to be copied to both modules. +;; In case of non-generative immutable globals, they are copied to whichever +;; modules they are used. $e will be copied to both module1 ($split1) and +;; module2 ($split2), and this will also cause $f to be copied to both modules. +;; However, generative immutable globals ($gen) cannot be copied, so $gen and +;; its dependency $gen-dep stay in the primary module, and only $gen is +;; exported/imported. (module + (type $struct (struct (field i32))) + (global $f i32 (i32.const 42)) (global $e i32 (global.get $f)) + (global $gen-dep i32 (i32.const 10)) + (global $gen (ref $struct) (struct.new $struct (global.get $gen-dep))) + (func $keep (nop) ) (func $split1 (drop (global.get $e)) + (drop (global.get $gen)) ) (func $split2 (drop (global.get $e)) + (drop (global.get $gen)) ) ) ;; PRIMARY: (module -;; PRIMARY-NEXT: (type $0 (func)) -;; PRIMARY-NEXT: (func $keep (type $0) +;; PRIMARY-NEXT: (type $struct (struct (field i32))) +;; PRIMARY-NEXT: (type $1 (func)) +;; PRIMARY-NEXT: (global $gen-dep i32 (i32.const 10)) +;; PRIMARY-NEXT: (global $gen (ref $struct) (struct.new $struct +;; PRIMARY-NEXT: (global.get $gen-dep) +;; PRIMARY-NEXT: )) +;; PRIMARY-NEXT: (export "global" (global $gen)) +;; PRIMARY-NEXT: (func $keep (type $1) ;; PRIMARY-NEXT: (nop) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) ;; SECONDARY1: (module -;; SECONDARY1-NEXT: (type $0 (func)) +;; SECONDARY1-NEXT: (type $0 (struct (field i32))) +;; SECONDARY1-NEXT: (type $1 (func)) +;; SECONDARY1-NEXT: (import "primary" "global" (global $gen (ref $0))) ;; SECONDARY1-NEXT: (global $f i32 (i32.const 42)) ;; SECONDARY1-NEXT: (global $e i32 (global.get $f)) -;; SECONDARY1-NEXT: (func $split1 (type $0) +;; SECONDARY1-NEXT: (func $split1 (type $1) ;; SECONDARY1-NEXT: (drop ;; SECONDARY1-NEXT: (global.get $e) ;; SECONDARY1-NEXT: ) +;; SECONDARY1-NEXT: (drop +;; SECONDARY1-NEXT: (global.get $gen) +;; SECONDARY1-NEXT: ) ;; SECONDARY1-NEXT: ) ;; SECONDARY1-NEXT: ) ;; SECONDARY2: (module -;; SECONDARY2-NEXT: (type $0 (func)) +;; SECONDARY2-NEXT: (type $0 (struct (field i32))) +;; SECONDARY2-NEXT: (type $1 (func)) +;; SECONDARY2-NEXT: (import "primary" "global" (global $gen (ref $0))) ;; SECONDARY2-NEXT: (global $f i32 (i32.const 42)) ;; SECONDARY2-NEXT: (global $e i32 (global.get $f)) -;; SECONDARY2-NEXT: (func $split2 (type $0) +;; SECONDARY2-NEXT: (func $split2 (type $1) ;; SECONDARY2-NEXT: (drop ;; SECONDARY2-NEXT: (global.get $e) ;; SECONDARY2-NEXT: ) +;; SECONDARY2-NEXT: (drop +;; SECONDARY2-NEXT: (global.get $gen) +;; SECONDARY2-NEXT: ) ;; SECONDARY2-NEXT: ) ;; SECONDARY2-NEXT: ) From d4adaf65ddde21b0490c137aa3cd3e387d13519f Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 9 Nov 2026 07:58:06 +0000 Subject: [PATCH 4/6] Test comments 80-col wrapping --- test/lit/wasm-split/split-module-items.wast | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index c475367d1c8..2cf526d1f36 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -129,7 +129,8 @@ (i32.const 0) ) ) - ;; Uses $keep-immutable-global, $keep-immutable-generative-global, and $keep-mutable-global + ;; Uses $keep-immutable-global, $keep-immutable-generative-global, and + ;; $keep-mutable-global (drop (global.get $keep-immutable-global) ) @@ -156,7 +157,8 @@ (i32.const 0) ) ) - ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and $shared-mutable-global + ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and + ;; $shared-mutable-global (drop (global.get $shared-immutable-global) ) @@ -198,7 +200,8 @@ (drop (table.get $keep-table2 (i32.const 0)) ) - ;; Uses $split-immutable-global, $split-immutable-generative-global, and $split-mutable-global + ;; Uses $split-immutable-global, $split-immutable-generative-global, and + ;; $split-mutable-global (drop (global.get $split-immutable-global) ) @@ -225,7 +228,8 @@ (i32.const 0) ) ) - ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and $shared-mutable-global + ;; Uses $shared-immutable-global, $shared-immutable-generative-global, and + ;; $shared-mutable-global (drop (global.get $shared-immutable-global) ) From 9f33c5158272cfd192ae084841eff303813799a9 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 9 Nov 2026 09:33:39 +0000 Subject: [PATCH 5/6] Add 'static' to canCopyGlobal --- 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 263b30e4c8d..fb682178b39 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -780,7 +780,7 @@ void ModuleSplitter::thunkExportedSecondaryFunctions() { } } -bool canCopyGlobal(Global* global, Module& wasm) { +static bool canCopyGlobal(Global* global, Module& wasm) { // Mutable globals cannot be copied to multiple modules because mutations in // one module would not be visible in others. Immutable globals with // generative initializers (e.g. struct.new) also cannot be copied because From 5685482ed93d6a8d4f366c2656a2756687398b1c Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 10 Nov 2026 06:34:45 +0000 Subject: [PATCH 6/6] Add comment that imported globals can be moved --- src/ir/module-splitting.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index fb682178b39..d0bf2e1206c 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -785,7 +785,8 @@ static bool canCopyGlobal(Global* global, Module& wasm) { // one module would not be visible in others. Immutable globals with // generative initializers (e.g. struct.new) also cannot be copied because // there would be an observable difference (e.g. struct identity tested with - // ref.eq) between the values in the copies. + // ref.eq) between the values in the copies. Note that imported globals can be + // copied / moved. return !global->mutable_ && (!global->init || !Properties::isGenerative(global->init, nullptr, wasm));