From 8939ccda8cc18e1de49dc31a6c1d400f4e6ddd38 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 6 Aug 2026 07:15:39 +0000 Subject: [PATCH 01/14] [wasm-split] Precompute ownership info (NFC) Given a module element name, many parts of the code queries for its owning modules (where the module element has to be placed) or secondary modules using that module element. This adds `OwnershipTracker`, which precomputes and manages that information. All calls to `getOwner` or `getUsingSecondaries` that required computations iterating on all secondary modules which can be as many as thousands, has been replaced with a call that simply returns prcomputed information. For the Jul 2026 version of the applications received from the Dart team, this reduces the running time of wasm-split by 17% for acx_gallery (30s -> 25s) and by 33% for essentials (230s -> 153s). Suggested in https://github.com/WebAssembly/binaryen/pull/8832#issuecomment-4714734931. --- src/ir/module-splitting.cpp | 324 +++++++++++++++++++++++------------- 1 file changed, 206 insertions(+), 118 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index fd01faef8e5..7a006a403f0 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 "support/small_vector.h" #include "support/stdckdint.h" #include "wasm-builder.h" #include "wasm.h" @@ -313,6 +314,130 @@ TableSlotManager::Slot TableSlotManager::getSlot(Name func, HeapType type) { return newSlot; } +// Module items ownership tracking + +// Struct containing sets of used module elements of a single module +struct UsedNames { + std::unordered_set globals; + std::unordered_set memories; + std::unordered_set tables; + std::unordered_set tags; + std::unordered_set dataSegments; + std::unordered_set elementSegments; +}; + +// A tracker that, given a module element, tracks which module is its owner, +// i.e., where the element should be placed, and the list of secondary modules +// using this element. +struct OwnershipTracker { + UsedNames primaryUsed; + std::vector secondaryUsed; + + struct ItemInfo { + UsedNames* owner = nullptr; + SmallVector usingSecondaries; + }; + + std::unordered_map tables; + std::unordered_map memories; + std::unordered_map globals; + std::unordered_map tags; + std::unordered_map dataSegments; + std::unordered_map elementSegments; + + std::unordered_map usedToSecondary; + + using FieldType = std::unordered_set UsedNames::*; + using MapType = std::unordered_map OwnershipTracker::*; + + void insert(Name name, UsedNames* owner, MapType mapField, FieldType field) { + (owner->*field).insert(name); + // Figure out which module the 'owner' is of this item. If it is used by a + // single 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. + auto [it, inserted] = (this->*mapField).insert({name, ItemInfo{owner, {}}}); + Module* mod = usedToSecondary[owner]; + if (inserted) { + if (mod) { + it->second.usingSecondaries.push_back(mod); + } + } else { + if (it->second.owner != owner) { + it->second.owner = &primaryUsed; + (primaryUsed.*field).insert(name); + } + if (mod) { + auto& vec = it->second.usingSecondaries; + if (std::find(vec.begin(), vec.end(), mod) == vec.end()) { + vec.push_back(mod); + } + } + } + } + + void build(const std::vector>& secondaries) { + usedToSecondary[&primaryUsed] = nullptr; + for (size_t i = 0; i < secondaryUsed.size(); ++i) { + usedToSecondary[&secondaryUsed[i]] = secondaries[i].get(); + } + + auto buildMap = [&](FieldType field, + std::unordered_map& map) { + for (auto& name : (primaryUsed.*field)) { + map[name].owner = &primaryUsed; + } + for (size_t i = 0; i < secondaryUsed.size(); ++i) { + auto& sec = secondaryUsed[i]; + auto* mod = secondaries[i].get(); + for (auto& name : (sec.*field)) { + auto [it, inserted] = map.insert({name, ItemInfo{&sec, {}}}); + it->second.usingSecondaries.push_back(mod); + if (!inserted) { + it->second.owner = &primaryUsed; + } + } + } + }; + buildMap(&UsedNames::tables, tables); + buildMap(&UsedNames::memories, memories); + buildMap(&UsedNames::globals, globals); + buildMap(&UsedNames::tags, tags); + buildMap(&UsedNames::dataSegments, dataSegments); + buildMap(&UsedNames::elementSegments, elementSegments); + } + + UsedNames* getOwner(Name name, + const std::unordered_map& map) { + auto it = map.find(name); + if (it != map.end()) { + return it->second.owner; + } + return nullptr; + } + + const SmallVector& + getUsingSecondaries(Name name, + const std::unordered_map& map) { + auto it = map.find(name); + if (it != map.end()) { + return it->second.usingSecondaries; + } + static SmallVector empty; + return empty; + } + + bool useEmpty(Name name, const std::unordered_map& map) { + return getOwner(name, map) == nullptr; + } + + bool usedBySingleSecondary(Name name, + const std::unordered_map& map) { + auto* owner = getOwner(name, map); + return owner != nullptr && owner != &primaryUsed; + } +}; + struct ModuleSplitter { const Config& config; std::vector> secondaries; @@ -359,17 +484,9 @@ struct ModuleSplitter { ExternalKind kind); Name getTrampoline(Name funcName); - struct UsedNames { - std::unordered_set globals; - std::unordered_set memories; - std::unordered_set tables; - std::unordered_set tags; - std::unordered_set dataSegments; - std::unordered_set elementSegments; - }; - using PrimarySecondaryUsedNames = - std::pair>; - PrimarySecondaryUsedNames computeUsedNames(); + OwnershipTracker tracker; + + void computeUsedNames(); // Main splitting steps void classifyFunctions(); @@ -649,12 +766,18 @@ void ModuleSplitter::thunkExportedSecondaryFunctions() { } } -ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { +void ModuleSplitter::computeUsedNames() { + UsedNames& primaryUsed = tracker.primaryUsed; + std::vector& secondaryUsed = tracker.secondaryUsed; + struct NameCollector : public PostWalker> { UsedNames& used; - NameCollector(UsedNames& used) : used(used) {} + OwnershipTracker* tracker = nullptr; + + NameCollector(UsedNames& used, OwnershipTracker* tracker = nullptr) + : used(used), tracker(tracker) {} void visitExpression(Expression* curr) { #define DELEGATE_ID curr->_id @@ -670,26 +793,36 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { #define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) #define DELEGATE_FIELD_ADDRESS(id, field) +// In the initial building phase, we just directly add to a UsedName struct. +// After OwnershipTracker is constructed, we all its insert() method to update +// owner modules and using secondary modules correctly. +#define ADD_ITEM(FIELD, VAL) \ + if (tracker) { \ + tracker->insert(VAL, &used, &OwnershipTracker::FIELD, &UsedNames::FIELD); \ + } else { \ + used.FIELD.insert(VAL); \ + } + #define DELEGATE_FIELD_NAME_KIND(id, field, kind) \ if (cast->field.is()) { \ switch (kind) { \ case ModuleItemKind::Table: \ - used.tables.insert(cast->field); \ + ADD_ITEM(tables, cast->field); \ break; \ case ModuleItemKind::Memory: \ - used.memories.insert(cast->field); \ + ADD_ITEM(memories, cast->field); \ break; \ case ModuleItemKind::Global: \ - used.globals.insert(cast->field); \ + ADD_ITEM(globals, cast->field); \ break; \ case ModuleItemKind::Tag: \ - used.tags.insert(cast->field); \ + ADD_ITEM(tags, cast->field); \ break; \ case ModuleItemKind::DataSegment: \ - used.dataSegments.insert(cast->field); \ + ADD_ITEM(dataSegments, cast->field); \ break; \ case ModuleItemKind::ElementSegment: \ - used.elementSegments.insert(cast->field); \ + ADD_ITEM(elementSegments, cast->field); \ break; \ case ModuleItemKind::Function: \ case ModuleItemKind::Invalid: \ @@ -698,6 +831,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } #include "wasm-delegations-fields.def" +#undef ADD_ITEM } }; @@ -727,8 +861,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { return used; }; - UsedNames primaryUsed = scanModule(primary); - std::vector secondaryUsed; + primaryUsed = scanModule(primary); for (auto& secondaryPtr : secondaries) { secondaryUsed.push_back(scanModule(*secondaryPtr)); } @@ -783,27 +916,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { } } - // 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; - if ((primaryUsed.*field).contains(name)) { - owner = &primaryUsed; - } - for (auto& sec : secondaryUsed) { - if ((sec.*field).contains(name)) { - if (owner) { - owner = &primaryUsed; - break; - } - owner = &sec; - } - } - return owner; - }; + tracker.build(secondaries); // Scan table initializers into their owning modules. If a table is used by a // single secondary module, its initializer dependencies are marked as "used" @@ -814,8 +927,8 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!table->init) { continue; } - if (UsedNames* owner = getOwner(table->name, &UsedNames::tables)) { - NameCollector(*owner).walk(table->init); + if (UsedNames* owner = tracker.getOwner(table->name, tracker.tables)) { + NameCollector(*owner, &tracker).walk(table->init); } } } @@ -864,13 +977,16 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { return false; }; +#define ADD_ITEM_TO_TRACKER(FIELD, VAL) \ + tracker.insert(VAL, owner, &OwnershipTracker::FIELD, &UsedNames::FIELD) + // Iterate on active data and element segments. If its table or memory is // used by a single secondary module, mark it "used" there. Only scan its // 'offset' or 'data'(in case of ElementSegment) and add it to that module's // used only when it is a sole secondary owner. If not assign it to the // primary module and scan it there. ModuleUtils::iterActiveDataSegments(primary, [&](DataSegment* segment) { - UsedNames* owner = getOwner(segment->memory, &UsedNames::memories); + UsedNames* owner = tracker.getOwner(segment->memory, tracker.memories); // Trapping segments should be kept in the primary module because they are // evaluated at the instantiation time. if (mayTrap(segment)) { @@ -879,15 +995,15 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!owner) { return; } - owner->dataSegments.insert(segment->name); - owner->memories.insert(segment->memory); + ADD_ITEM_TO_TRACKER(dataSegments, segment->name); + ADD_ITEM_TO_TRACKER(memories, segment->memory); if (segment->offset) { - NameCollector(*owner).walk(segment->offset); + NameCollector(*owner, &tracker).walk(segment->offset); } }); ModuleUtils::iterActiveElementSegments(primary, [&](ElementSegment* segment) { - UsedNames* owner = getOwner(segment->table, &UsedNames::tables); + UsedNames* owner = tracker.getOwner(segment->table, tracker.tables); // If placeholders are NOT used, and if all functions in an element segment // belong to a single secondary module, we can move the segment to that @@ -934,13 +1050,13 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!owner) { return; } - owner->elementSegments.insert(segment->name); - owner->tables.insert(segment->table); + ADD_ITEM_TO_TRACKER(elementSegments, segment->name); + ADD_ITEM_TO_TRACKER(tables, segment->table); if (segment->offset) { - NameCollector(*owner).walk(segment->offset); + NameCollector(*owner, &tracker).walk(segment->offset); } for (auto* item : segment->data) { - NameCollector(*owner).walk(item); + NameCollector(*owner, &tracker).walk(item); } }); @@ -952,7 +1068,7 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (segment->isPassive() && primaryUsed.elementSegments.contains(segment->name)) { for (auto* item : segment->data) { - NameCollector(primaryUsed).walk(item); + NameCollector(primaryUsed, &tracker).walk(item); } } } @@ -969,32 +1085,16 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { if (!global->init) { continue; } - if (UsedNames* owner = getOwner(global->name, &UsedNames::globals)) { + if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { for (auto* get : FindAll(global->init).list) { - owner->globals.insert(get->name); + ADD_ITEM_TO_TRACKER(globals, get->name); } } } - - return std::make_pair(primaryUsed, secondaryUsed); } void ModuleSplitter::shareImportableItems() { - auto usedNames = computeUsedNames(); - auto& primaryUsed = usedNames.first; - auto& secondaryUsed = usedNames.second; - - // Given a name and module item kind, returns the list of secondary modules - // using that name - auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { - std::vector usingModules; - for (size_t i = 0; i < secondaries.size(); ++i) { - if ((secondaryUsed[i].*field).contains(name)) { - usingModules.push_back(secondaries[i].get()); - } - } - return usingModules; - }; + computeUsedNames(); // Share module items with secondary modules. // 1. Only share an item with the modules that use it @@ -1005,18 +1105,16 @@ void ModuleSplitter::shareImportableItems() { std::vector memoriesToRemove; for (auto& memory : primary.memories) { - auto usingSecondaries = - getUsingSecondaries(memory->name, &UsedNames::memories); - bool inPrimary = primaryUsed.memories.contains(memory->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(memory->name, tracker.memories)) { memoriesToRemove.push_back(memory->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(memory->name, tracker.memories)) { + auto* secondary = + tracker.getUsingSecondaries(memory->name, tracker.memories)[0]; ModuleUtils::copyMemory(memory.get(), *secondary); memoriesToRemove.push_back(memory->name); } else { - for (auto* secondary : usingSecondaries) { + for (auto* secondary : + tracker.getUsingSecondaries(memory->name, tracker.memories)) { auto* secondaryMemory = ModuleUtils::copyMemory(memory.get(), *secondary); makeImportExport( @@ -1030,19 +1128,17 @@ void ModuleSplitter::shareImportableItems() { std::vector tablesToRemove; for (auto& table : primary.tables) { - auto usingSecondaries = - getUsingSecondaries(table->name, &UsedNames::tables); - bool inPrimary = primaryUsed.tables.contains(table->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(table->name, tracker.tables)) { tablesToRemove.push_back(table->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(table->name, tracker.tables)) { + auto* secondary = + tracker.getUsingSecondaries(table->name, tracker.tables)[0]; assert(!secondary->getTableOrNull(table->name)); ModuleUtils::copyTable(table.get(), *secondary); tablesToRemove.push_back(table->name); } else { - for (auto* secondary : usingSecondaries) { + for (auto* secondary : + tracker.getUsingSecondaries(table->name, tracker.tables)) { auto* secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); } @@ -1059,18 +1155,16 @@ void ModuleSplitter::shareImportableItems() { "TODO: add wrapper functions for disallowed mutable globals"); } - auto usingSecondaries = - getUsingSecondaries(global->name, &UsedNames::globals); - bool inPrimary = primaryUsed.globals.contains(global->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(global->name, tracker.globals)) { globalsToRemove.push_back(global->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(global->name, tracker.globals)) { + auto* secondary = + tracker.getUsingSecondaries(global->name, tracker.globals)[0]; ModuleUtils::copyGlobal(global.get(), *secondary); globalsToRemove.push_back(global->name); } else { - for (auto* secondary : usingSecondaries) { + for (auto* secondary : + tracker.getUsingSecondaries(global->name, tracker.globals)) { auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); makeImportExport( @@ -1084,17 +1178,15 @@ void ModuleSplitter::shareImportableItems() { std::vector tagsToRemove; for (auto& tag : primary.tags) { - auto usingSecondaries = getUsingSecondaries(tag->name, &UsedNames::tags); - bool inPrimary = primaryUsed.tags.contains(tag->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(tag->name, tracker.tags)) { tagsToRemove.push_back(tag->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(tag->name, tracker.tags)) { + auto* secondary = tracker.getUsingSecondaries(tag->name, tracker.tags)[0]; ModuleUtils::copyTag(tag.get(), *secondary); tagsToRemove.push_back(tag->name); } else { - for (auto* secondary : usingSecondaries) { + for (auto* secondary : + tracker.getUsingSecondaries(tag->name, tracker.tags)) { auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); } @@ -1110,14 +1202,12 @@ void ModuleSplitter::shareImportableItems() { std::vector dataSegmentsToRemove; for (auto& dataSegment : primary.dataSegments) { - auto usingSecondaries = - getUsingSecondaries(dataSegment->name, &UsedNames::dataSegments); - bool inPrimary = primaryUsed.dataSegments.contains(dataSegment->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(dataSegment->name, tracker.dataSegments)) { dataSegmentsToRemove.push_back(dataSegment->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(dataSegment->name, + tracker.dataSegments)) { + auto* secondary = + tracker.getUsingSecondaries(dataSegment->name, tracker.dataSegments)[0]; ModuleUtils::copyDataSegment(dataSegment.get(), *secondary); dataSegmentsToRemove.push_back(dataSegment->name); } @@ -1128,14 +1218,12 @@ void ModuleSplitter::shareImportableItems() { std::vector elementSegmentsToRemove; for (auto& elementSegment : primary.elementSegments) { - auto usingSecondaries = - getUsingSecondaries(elementSegment->name, &UsedNames::elementSegments); - bool inPrimary = primaryUsed.elementSegments.contains(elementSegment->name); - - if (!inPrimary && usingSecondaries.empty()) { + if (tracker.useEmpty(elementSegment->name, tracker.elementSegments)) { elementSegmentsToRemove.push_back(elementSegment->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; + } else if (tracker.usedBySingleSecondary(elementSegment->name, + tracker.elementSegments)) { + auto* secondary = tracker.getUsingSecondaries(elementSegment->name, + tracker.elementSegments)[0]; ModuleUtils::copyElementSegment(elementSegment.get(), *secondary); elementSegmentsToRemove.push_back(elementSegment->name); } From 4e9ea51bfab475beb390631f6d11fa5c3c5dc4f0 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sat, 8 Aug 2026 05:28:11 +0000 Subject: [PATCH 02/14] [wasm-split] Remove module elements in bulk (NFC) Previously we removed module elements one by one within a loop. But because `Module` stores a module element in both a map and a vector, removing a single module element using `removeModuleElement` is O(N), because it needs to shift all vector elements after it: https://github.com/WebAssembly/binaryen/blob/302396a676433152a32375a81d71e74687c97a1b/src/wasm/wasm.cpp#L1970-L1979 This removes module elements in bulk using `removeModuleElements`, which does the shifting only once. https://github.com/WebAssembly/binaryen/blob/302396a676433152a32375a81d71e74687c97a1b/src/wasm/wasm.cpp#L2004-L2018 Combining with #8986, acx_gallery's running time improved by 50.3% (30s -> 15s), and essentials by 60.8% (230s -> 90s). (for Jul 2026 version) I guess the main reason for the running time increase in #8441 was this O(N) `removeModuleElement` called within a loop after all. --- src/ir/module-splitting.cpp | 67 +++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 7a006a403f0..b08a6ea4006 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1103,15 +1103,15 @@ void ModuleSplitter::shareImportableItems() { // the primary and secondary modules), export the item from the primary and // import it from the using secondary modules. - std::vector memoriesToRemove; + std::unordered_set memoriesToRemove; for (auto& memory : primary.memories) { if (tracker.useEmpty(memory->name, tracker.memories)) { - memoriesToRemove.push_back(memory->name); + memoriesToRemove.insert(memory->name); } else if (tracker.usedBySingleSecondary(memory->name, tracker.memories)) { auto* secondary = tracker.getUsingSecondaries(memory->name, tracker.memories)[0]; ModuleUtils::copyMemory(memory.get(), *secondary); - memoriesToRemove.push_back(memory->name); + memoriesToRemove.insert(memory->name); } else { for (auto* secondary : tracker.getUsingSecondaries(memory->name, tracker.memories)) { @@ -1122,20 +1122,19 @@ void ModuleSplitter::shareImportableItems() { } } } - for (auto& name : memoriesToRemove) { - primary.removeMemory(name); - } + primary.removeMemories( + [&](Memory* memory) { return memoriesToRemove.count(memory->name); }); - std::vector tablesToRemove; + std::unordered_set tablesToRemove; for (auto& table : primary.tables) { if (tracker.useEmpty(table->name, tracker.tables)) { - tablesToRemove.push_back(table->name); + tablesToRemove.insert(table->name); } else if (tracker.usedBySingleSecondary(table->name, tracker.tables)) { auto* secondary = tracker.getUsingSecondaries(table->name, tracker.tables)[0]; assert(!secondary->getTableOrNull(table->name)); ModuleUtils::copyTable(table.get(), *secondary); - tablesToRemove.push_back(table->name); + tablesToRemove.insert(table->name); } else { for (auto* secondary : tracker.getUsingSecondaries(table->name, tracker.tables)) { @@ -1144,11 +1143,10 @@ void ModuleSplitter::shareImportableItems() { } } } - for (auto& name : tablesToRemove) { - primary.removeTable(name); - } + primary.removeTables( + [&](Table* table) { return tablesToRemove.count(table->name); }); - std::vector globalsToRemove; + std::unordered_set globalsToRemove; for (auto& global : primary.globals) { if (global->mutable_) { assert(primary.features.hasMutableGlobals() && @@ -1156,12 +1154,12 @@ void ModuleSplitter::shareImportableItems() { } if (tracker.useEmpty(global->name, tracker.globals)) { - globalsToRemove.push_back(global->name); + globalsToRemove.insert(global->name); } else if (tracker.usedBySingleSecondary(global->name, tracker.globals)) { auto* secondary = tracker.getUsingSecondaries(global->name, tracker.globals)[0]; ModuleUtils::copyGlobal(global.get(), *secondary); - globalsToRemove.push_back(global->name); + globalsToRemove.insert(global->name); } else { for (auto* secondary : tracker.getUsingSecondaries(global->name, tracker.globals)) { @@ -1172,18 +1170,17 @@ void ModuleSplitter::shareImportableItems() { } } } - for (auto& name : globalsToRemove) { - primary.removeGlobal(name); - } + primary.removeGlobals( + [&](Global* global) { return globalsToRemove.count(global->name); }); - std::vector tagsToRemove; + std::unordered_set tagsToRemove; for (auto& tag : primary.tags) { if (tracker.useEmpty(tag->name, tracker.tags)) { - tagsToRemove.push_back(tag->name); + tagsToRemove.insert(tag->name); } else if (tracker.usedBySingleSecondary(tag->name, tracker.tags)) { auto* secondary = tracker.getUsingSecondaries(tag->name, tracker.tags)[0]; ModuleUtils::copyTag(tag.get(), *secondary); - tagsToRemove.push_back(tag->name); + tagsToRemove.insert(tag->name); } else { for (auto* secondary : tracker.getUsingSecondaries(tag->name, tracker.tags)) { @@ -1192,45 +1189,43 @@ void ModuleSplitter::shareImportableItems() { } } } - for (auto& name : tagsToRemove) { - primary.removeTag(name); - } + primary.removeTags([&](Tag* tag) { return tagsToRemove.count(tag->name); }); // Move segments that are exclusively used in a secondary module. If not, do // nothing. (Segments cannot be imported / exported. They will be handled in // indirectReferencesToSecondaryFunctions.) - std::vector dataSegmentsToRemove; + std::unordered_set dataSegmentsToRemove; for (auto& dataSegment : primary.dataSegments) { if (tracker.useEmpty(dataSegment->name, tracker.dataSegments)) { - dataSegmentsToRemove.push_back(dataSegment->name); + dataSegmentsToRemove.insert(dataSegment->name); } else if (tracker.usedBySingleSecondary(dataSegment->name, tracker.dataSegments)) { auto* secondary = tracker.getUsingSecondaries(dataSegment->name, tracker.dataSegments)[0]; ModuleUtils::copyDataSegment(dataSegment.get(), *secondary); - dataSegmentsToRemove.push_back(dataSegment->name); + dataSegmentsToRemove.insert(dataSegment->name); } } - for (auto& name : dataSegmentsToRemove) { - primary.removeDataSegment(name); - } + primary.removeDataSegments([&](DataSegment* dataSegment) { + return dataSegmentsToRemove.count(dataSegment->name); + }); - std::vector elementSegmentsToRemove; + std::unordered_set elementSegmentsToRemove; for (auto& elementSegment : primary.elementSegments) { if (tracker.useEmpty(elementSegment->name, tracker.elementSegments)) { - elementSegmentsToRemove.push_back(elementSegment->name); + elementSegmentsToRemove.insert(elementSegment->name); } else if (tracker.usedBySingleSecondary(elementSegment->name, tracker.elementSegments)) { auto* secondary = tracker.getUsingSecondaries(elementSegment->name, tracker.elementSegments)[0]; ModuleUtils::copyElementSegment(elementSegment.get(), *secondary); - elementSegmentsToRemove.push_back(elementSegment->name); + elementSegmentsToRemove.insert(elementSegment->name); } } - for (auto& name : elementSegmentsToRemove) { - primary.removeElementSegment(name); - } + primary.removeElementSegments([&](ElementSegment* elementSegment) { + return elementSegmentsToRemove.count(elementSegment->name); + }); } void ModuleSplitter::indirectReferencesToSecondaryFunctions() { From 752739b4d9967db1768988367ce1015627c29773 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 12 Aug 2026 07:11:25 +0000 Subject: [PATCH 03/14] [wasm-split] Deduplicate loops in shareImportableItems (NFC) We have six mostly identical loops in `shareImportableItems`, each for memories, tables, globals, tags, data segments, and element segments. This factors the core logic out as a generic lambda function. --- src/ir/module-splitting.cpp | 173 ++++++++++++++---------------------- 1 file changed, 65 insertions(+), 108 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index b08a6ea4006..e53a8ddb808 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1103,129 +1103,86 @@ void ModuleSplitter::shareImportableItems() { // the primary and secondary modules), export the item from the primary and // import it from the using secondary modules. - std::unordered_set memoriesToRemove; - for (auto& memory : primary.memories) { - if (tracker.useEmpty(memory->name, tracker.memories)) { - memoriesToRemove.insert(memory->name); - } else if (tracker.usedBySingleSecondary(memory->name, tracker.memories)) { - auto* secondary = - tracker.getUsingSecondaries(memory->name, tracker.memories)[0]; - ModuleUtils::copyMemory(memory.get(), *secondary); - memoriesToRemove.insert(memory->name); - } else { - for (auto* secondary : - tracker.getUsingSecondaries(memory->name, tracker.memories)) { - auto* secondaryMemory = - ModuleUtils::copyMemory(memory.get(), *secondary); - makeImportExport( - *memory, *secondaryMemory, "memory", ExternalKind::Memory); - } - } - } - primary.removeMemories( - [&](Memory* memory) { return memoriesToRemove.count(memory->name); }); - - std::unordered_set tablesToRemove; - for (auto& table : primary.tables) { - if (tracker.useEmpty(table->name, tracker.tables)) { - tablesToRemove.insert(table->name); - } else if (tracker.usedBySingleSecondary(table->name, tracker.tables)) { - auto* secondary = - tracker.getUsingSecondaries(table->name, tracker.tables)[0]; - assert(!secondary->getTableOrNull(table->name)); - ModuleUtils::copyTable(table.get(), *secondary); - tablesToRemove.insert(table->name); - } else { - for (auto* secondary : - tracker.getUsingSecondaries(table->name, tracker.tables)) { - auto* secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); - makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); + auto shareElements = [&](auto& elements, + auto& trackerElements, + auto copyElement, + auto removeElements, + const char* exportName = nullptr, + ExternalKind kind = ExternalKind::Invalid) { + std::unordered_set elementsToRemove; + for (auto& element : elements) { + if (tracker.useEmpty(element->name, trackerElements)) { + elementsToRemove.insert(element->name); + } else if (tracker.usedBySingleSecondary(element->name, + trackerElements)) { + auto* secondary = + tracker.getUsingSecondaries(element->name, trackerElements)[0]; + copyElement(element.get(), *secondary); + elementsToRemove.insert(element->name); + } else { + // 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)) { + auto* secondaryElement = copyElement(element.get(), *secondary); + makeImportExport(*element, *secondaryElement, exportName, kind); + } + } } } - } - primary.removeTables( - [&](Table* table) { return tablesToRemove.count(table->name); }); + (primary.*removeElements)( + [&](auto* element) { return elementsToRemove.count(element->name); }); + }; + + shareElements(primary.memories, + tracker.memories, + ModuleUtils::copyMemory, + &Module::removeMemories, + "memory", + ExternalKind::Memory); + + shareElements(primary.tables, + tracker.tables, + ModuleUtils::copyTable, + &Module::removeTables, + "table", + ExternalKind::Table); - std::unordered_set globalsToRemove; for (auto& global : primary.globals) { if (global->mutable_) { assert(primary.features.hasMutableGlobals() && "TODO: add wrapper functions for disallowed mutable globals"); } - - if (tracker.useEmpty(global->name, tracker.globals)) { - globalsToRemove.insert(global->name); - } else if (tracker.usedBySingleSecondary(global->name, tracker.globals)) { - auto* secondary = - tracker.getUsingSecondaries(global->name, tracker.globals)[0]; - ModuleUtils::copyGlobal(global.get(), *secondary); - globalsToRemove.insert(global->name); - } else { - for (auto* secondary : - tracker.getUsingSecondaries(global->name, tracker.globals)) { - auto* secondaryGlobal = - ModuleUtils::copyGlobal(global.get(), *secondary); - makeImportExport( - *global, *secondaryGlobal, "global", ExternalKind::Global); - } - } - } - primary.removeGlobals( - [&](Global* global) { return globalsToRemove.count(global->name); }); - - std::unordered_set tagsToRemove; - for (auto& tag : primary.tags) { - if (tracker.useEmpty(tag->name, tracker.tags)) { - tagsToRemove.insert(tag->name); - } else if (tracker.usedBySingleSecondary(tag->name, tracker.tags)) { - auto* secondary = tracker.getUsingSecondaries(tag->name, tracker.tags)[0]; - ModuleUtils::copyTag(tag.get(), *secondary); - tagsToRemove.insert(tag->name); - } else { - for (auto* secondary : - tracker.getUsingSecondaries(tag->name, tracker.tags)) { - auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); - makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); - } - } } - primary.removeTags([&](Tag* tag) { return tagsToRemove.count(tag->name); }); + shareElements(primary.globals, + tracker.globals, + ModuleUtils::copyGlobal, + &Module::removeGlobals, + "global", + ExternalKind::Global); + + shareElements(primary.tags, + tracker.tags, + ModuleUtils::copyTag, + &Module::removeTags, + "tag", + ExternalKind::Tag); // Move segments that are exclusively used in a secondary module. If not, do // nothing. (Segments cannot be imported / exported. They will be handled in // indirectReferencesToSecondaryFunctions.) - std::unordered_set dataSegmentsToRemove; - for (auto& dataSegment : primary.dataSegments) { - if (tracker.useEmpty(dataSegment->name, tracker.dataSegments)) { - dataSegmentsToRemove.insert(dataSegment->name); - } else if (tracker.usedBySingleSecondary(dataSegment->name, - tracker.dataSegments)) { - auto* secondary = - tracker.getUsingSecondaries(dataSegment->name, tracker.dataSegments)[0]; - ModuleUtils::copyDataSegment(dataSegment.get(), *secondary); - dataSegmentsToRemove.insert(dataSegment->name); - } - } - primary.removeDataSegments([&](DataSegment* dataSegment) { - return dataSegmentsToRemove.count(dataSegment->name); - }); + shareElements(primary.dataSegments, + tracker.dataSegments, + ModuleUtils::copyDataSegment, + &Module::removeDataSegments); - std::unordered_set elementSegmentsToRemove; - for (auto& elementSegment : primary.elementSegments) { - if (tracker.useEmpty(elementSegment->name, tracker.elementSegments)) { - elementSegmentsToRemove.insert(elementSegment->name); - } else if (tracker.usedBySingleSecondary(elementSegment->name, - tracker.elementSegments)) { - auto* secondary = tracker.getUsingSecondaries(elementSegment->name, - tracker.elementSegments)[0]; - ModuleUtils::copyElementSegment(elementSegment.get(), *secondary); - elementSegmentsToRemove.insert(elementSegment->name); - } - } - primary.removeElementSegments([&](ElementSegment* elementSegment) { - return elementSegmentsToRemove.count(elementSegment->name); - }); + shareElements(primary.elementSegments, + tracker.elementSegments, + ModuleUtils::copyElementSegment, + &Module::removeElementSegments); } void ModuleSplitter::indirectReferencesToSecondaryFunctions() { From cbe6fc3125df9d58f479a943d122e9ce4043461a Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 14:25:41 -0700 Subject: [PATCH 04/14] Update src/ir/module-splitting.cpp Co-authored-by: Thomas Lively --- 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 7a006a403f0..936b8b4ad2e 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -796,7 +796,7 @@ void ModuleSplitter::computeUsedNames() { // In the initial building phase, we just directly add to a UsedName struct. // After OwnershipTracker is constructed, we all its insert() method to update // owner modules and using secondary modules correctly. -#define ADD_ITEM(FIELD, VAL) \ +#define ADD_ITEM(field, val) \ if (tracker) { \ tracker->insert(VAL, &used, &OwnershipTracker::FIELD, &UsedNames::FIELD); \ } else { \ From ba7088057743dcda52b284e044a2a75f8ac2f29f Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 21:29:14 +0000 Subject: [PATCH 05/14] Remove usedToSecondary map --- src/ir/module-splitting.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 936b8b4ad2e..f3661ea3c65 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -345,7 +345,7 @@ struct OwnershipTracker { std::unordered_map dataSegments; std::unordered_map elementSegments; - std::unordered_map usedToSecondary; + const std::vector>* secondaries = nullptr; using FieldType = std::unordered_set UsedNames::*; using MapType = std::unordered_map OwnershipTracker::*; @@ -357,30 +357,31 @@ 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, {}}}); - Module* mod = usedToSecondary[owner]; + Module* secondary = nullptr; + if (owner != &primaryUsed) { + size_t index = owner - secondaryUsed.data(); + secondary = (*secondaries)[index].get(); + } if (inserted) { - if (mod) { - it->second.usingSecondaries.push_back(mod); + if (secondary) { + it->second.usingSecondaries.push_back(secondary); } } else { if (it->second.owner != owner) { it->second.owner = &primaryUsed; (primaryUsed.*field).insert(name); } - if (mod) { + if (secondary) { auto& vec = it->second.usingSecondaries; - if (std::find(vec.begin(), vec.end(), mod) == vec.end()) { - vec.push_back(mod); + if (std::find(vec.begin(), vec.end(), secondary) == vec.end()) { + vec.push_back(secondary); } } } } void build(const std::vector>& secondaries) { - usedToSecondary[&primaryUsed] = nullptr; - for (size_t i = 0; i < secondaryUsed.size(); ++i) { - usedToSecondary[&secondaryUsed[i]] = secondaries[i].get(); - } + this->secondaries = &secondaries; auto buildMap = [&](FieldType field, std::unordered_map& map) { From 8a3cd6c219557eb5a17bd3a19b013dba0f51b001 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 21:33:10 +0000 Subject: [PATCH 06/14] FIELD->field, VAL->val --- src/ir/module-splitting.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index f3661ea3c65..39ad4a9d9a8 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -799,9 +799,9 @@ void ModuleSplitter::computeUsedNames() { // owner modules and using secondary modules correctly. #define ADD_ITEM(field, val) \ if (tracker) { \ - tracker->insert(VAL, &used, &OwnershipTracker::FIELD, &UsedNames::FIELD); \ + tracker->insert(val, &used, &OwnershipTracker::field, &UsedNames::field); \ } else { \ - used.FIELD.insert(VAL); \ + used.field.insert(val); \ } #define DELEGATE_FIELD_NAME_KIND(id, field, kind) \ From 491367ca038c46de4257465fc6ff94fe1ee7a342 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 21:58:54 +0000 Subject: [PATCH 07/14] Rename a few variables --- src/ir/module-splitting.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 39ad4a9d9a8..524090b9156 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -389,11 +389,11 @@ struct OwnershipTracker { map[name].owner = &primaryUsed; } for (size_t i = 0; i < secondaryUsed.size(); ++i) { - auto& sec = secondaryUsed[i]; - auto* mod = secondaries[i].get(); - for (auto& name : (sec.*field)) { - auto [it, inserted] = map.insert({name, ItemInfo{&sec, {}}}); - it->second.usingSecondaries.push_back(mod); + auto& secUsed = secondaryUsed[i]; + auto* secondary = secondaries[i].get(); + for (auto& name : (secUsed.*field)) { + auto [it, inserted] = map.insert({name, ItemInfo{&secUsed, {}}}); + it->second.usingSecondaries.push_back(secondary); if (!inserted) { it->second.owner = &primaryUsed; } From e39e9060f5bdab9c5f22bf5d525e6315af9f9231 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 22:02:36 +0000 Subject: [PATCH 08/14] More comments --- src/ir/module-splitting.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 524090b9156..c2fd442bc11 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -350,6 +350,10 @@ struct OwnershipTracker { using FieldType = std::unordered_set UsedNames::*; using MapType = std::unordered_map OwnershipTracker::*; + // 'mapField' points to one of OwnershipTracker's maps, such as + // std::unordered_map globals; + // 'field' points to one of UsedName's sets, such as + // std::unordered_set globals; void insert(Name name, UsedNames* owner, MapType mapField, FieldType field) { (owner->*field).insert(name); // Figure out which module the 'owner' is of this item. If it is used by a @@ -383,6 +387,10 @@ struct OwnershipTracker { void build(const std::vector>& secondaries) { this->secondaries = &secondaries; + // Build initial maps of a module element Name to an ItemInfo for each + // module element type. + // 'field' points to one of UsedName's sets, such as + // std::unordered_set globals; auto buildMap = [&](FieldType field, std::unordered_map& map) { for (auto& name : (primaryUsed.*field)) { From 3731f287571a770432375114f8944645e39e559f Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 16:48:45 -0700 Subject: [PATCH 09/14] Apply suggestions from code review Co-authored-by: Thomas Lively --- src/ir/module-splitting.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index c2fd442bc11..27c0ee09421 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -356,7 +356,7 @@ struct OwnershipTracker { // std::unordered_set globals; void insert(Name name, UsedNames* owner, MapType mapField, FieldType field) { (owner->*field).insert(name); - // Figure out which module the 'owner' is of this item. If it is used by a + // Figure out which module is the 'owner' of this item. If it is used by a // single 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. @@ -803,7 +803,7 @@ void ModuleSplitter::computeUsedNames() { #define DELEGATE_FIELD_ADDRESS(id, field) // In the initial building phase, we just directly add to a UsedName struct. -// After OwnershipTracker is constructed, we all its insert() method to update +// After OwnershipTracker is constructed, we call its insert() method to update // owner modules and using secondary modules correctly. #define ADD_ITEM(field, val) \ if (tracker) { \ From 529b1305a0d211a4bccef4e8445be025bb2d040d Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sat, 15 Aug 2026 02:22:45 +0000 Subject: [PATCH 10/14] [wasm-split] Don't use ParallelFunctionAnalysis in scanModule (NFC) This removes the use of `ParallelFunctionAnalysis` within `scanModule` (in `computeUsedNames`), which scans `UsedNames` for each module. I'm not 100% sure why but this improves running time at least for Dart applications. I also previously tried to use `ParallelFunctionAnalysis` in other functions but it resulted in slowdown so didn't do it. Maybe cache locality works against the parallelism. This reduces running time of acx_gallery (Jul 2026) by 7.8% (30.6s -> 28.2s) essentials by 4.2% (225.1s -> 215.6s). --- src/ir/module-splitting.cpp | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index fd01faef8e5..21695f0bf07 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -704,26 +704,12 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { // Given a module, collect names used in the module auto scanModule = [&](Module& module) { UsedNames used; - ModuleUtils::ParallelFunctionAnalysis nameCollector( - module, [&](Function* func, UsedNames& used) { - if (!func->imported()) { - NameCollector(used).walk(func->body); - } - }); - - for (auto& [_, funcUsed] : nameCollector.map) { - used.globals.insert(funcUsed.globals.begin(), funcUsed.globals.end()); - used.memories.insert(funcUsed.memories.begin(), funcUsed.memories.end()); - used.tables.insert(funcUsed.tables.begin(), funcUsed.tables.end()); - used.tags.insert(funcUsed.tags.begin(), funcUsed.tags.end()); - used.dataSegments.insert(funcUsed.dataSegments.begin(), - funcUsed.dataSegments.end()); - used.elementSegments.insert(funcUsed.elementSegments.begin(), - funcUsed.elementSegments.end()); - } - NameCollector collector(used); - + for (auto& func : module.functions) { + if (!func->imported()) { + collector.walk(func->body); + } + } return used; }; From f03574fd9cb06b35b118fe07e5e7f9c9acdbb900 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sat, 15 Aug 2026 03:32:47 +0000 Subject: [PATCH 11/14] Eliminate two-phase approach in computeUsedNames --- src/ir/module-splitting.cpp | 85 +++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index b535acb9c44..6ef3a682d7d 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -776,16 +776,21 @@ void ModuleSplitter::thunkExportedSecondaryFunctions() { } void ModuleSplitter::computeUsedNames() { + tracker.secondaries = &secondaries; + tracker.secondaryUsed.resize(secondaries.size()); UsedNames& primaryUsed = tracker.primaryUsed; std::vector& secondaryUsed = tracker.secondaryUsed; +#define ADD_ITEM_TO_TRACKER(field, val, owner) \ + tracker.insert(val, owner, &OwnershipTracker::field, &UsedNames::field) + struct NameCollector : public PostWalker> { UsedNames& used; - OwnershipTracker* tracker = nullptr; + OwnershipTracker& tracker; - NameCollector(UsedNames& used, OwnershipTracker* tracker = nullptr) + NameCollector(UsedNames& used, OwnershipTracker& tracker) : used(used), tracker(tracker) {} void visitExpression(Expression* curr) { @@ -802,36 +807,26 @@ void ModuleSplitter::computeUsedNames() { #define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) #define DELEGATE_FIELD_ADDRESS(id, field) -// In the initial building phase, we just directly add to a UsedName struct. -// After OwnershipTracker is constructed, we call its insert() method to update -// owner modules and using secondary modules correctly. -#define ADD_ITEM(field, val) \ - if (tracker) { \ - tracker->insert(val, &used, &OwnershipTracker::field, &UsedNames::field); \ - } else { \ - used.field.insert(val); \ - } - #define DELEGATE_FIELD_NAME_KIND(id, field, kind) \ if (cast->field.is()) { \ switch (kind) { \ case ModuleItemKind::Table: \ - ADD_ITEM(tables, cast->field); \ + ADD_ITEM_TO_TRACKER(tables, cast->field, &used); \ break; \ case ModuleItemKind::Memory: \ - ADD_ITEM(memories, cast->field); \ + ADD_ITEM_TO_TRACKER(memories, cast->field, &used); \ break; \ case ModuleItemKind::Global: \ - ADD_ITEM(globals, cast->field); \ + ADD_ITEM_TO_TRACKER(globals, cast->field, &used); \ break; \ case ModuleItemKind::Tag: \ - ADD_ITEM(tags, cast->field); \ + ADD_ITEM_TO_TRACKER(tags, cast->field, &used); \ break; \ case ModuleItemKind::DataSegment: \ - ADD_ITEM(dataSegments, cast->field); \ + ADD_ITEM_TO_TRACKER(dataSegments, cast->field, &used); \ break; \ case ModuleItemKind::ElementSegment: \ - ADD_ITEM(elementSegments, cast->field); \ + ADD_ITEM_TO_TRACKER(elementSegments, cast->field, &used); \ break; \ case ModuleItemKind::Function: \ case ModuleItemKind::Invalid: \ @@ -840,25 +835,22 @@ void ModuleSplitter::computeUsedNames() { } #include "wasm-delegations-fields.def" -#undef ADD_ITEM } }; // Given a module, collect names used in the module - auto scanModule = [&](Module& module) { - UsedNames used; - NameCollector collector(used); + auto scanModule = [&](Module& module, UsedNames& used) { + NameCollector collector(used, tracker); for (auto& func : module.functions) { if (!func->imported()) { collector.walk(func->body); } } - return used; }; - primaryUsed = scanModule(primary); - for (auto& secondaryPtr : secondaries) { - secondaryUsed.push_back(scanModule(*secondaryPtr)); + scanModule(primary, primaryUsed); + for (size_t i = 0; i < secondaries.size(); ++i) { + scanModule(*secondaries[i], secondaryUsed[i]); } // If primary module has exports, they are "used" in it. Secondary modules @@ -866,16 +858,16 @@ void ModuleSplitter::computeUsedNames() { for (auto& ex : primary.exports) { switch (ex->kind) { case ExternalKind::Global: - primaryUsed.globals.insert(*ex->getInternalName()); + ADD_ITEM_TO_TRACKER(globals, *ex->getInternalName(), &primaryUsed); break; case ExternalKind::Memory: - primaryUsed.memories.insert(*ex->getInternalName()); + ADD_ITEM_TO_TRACKER(memories, *ex->getInternalName(), &primaryUsed); break; case ExternalKind::Table: - primaryUsed.tables.insert(*ex->getInternalName()); + ADD_ITEM_TO_TRACKER(tables, *ex->getInternalName(), &primaryUsed); break; case ExternalKind::Tag: - primaryUsed.tags.insert(*ex->getInternalName()); + ADD_ITEM_TO_TRACKER(tags, *ex->getInternalName(), &primaryUsed); break; default: break; @@ -885,10 +877,10 @@ void ModuleSplitter::computeUsedNames() { // We need to assume the dispatch table and its base global are used in the // primary module, because we will create segments there later. if (tableManager.dispatchTable) { - primaryUsed.tables.insert(tableManager.dispatchTable->name); + ADD_ITEM_TO_TRACKER(tables, tableManager.dispatchTable->name, &primaryUsed); } if (tableManager.dispatchBase.global) { - primaryUsed.globals.insert(tableManager.dispatchBase.global); + ADD_ITEM_TO_TRACKER(globals, tableManager.dispatchBase.global, &primaryUsed); } // If custom-descirptors is enabled, global and table initializers can trap. @@ -899,20 +891,18 @@ void ModuleSplitter::computeUsedNames() { if (global->init && EffectAnalyzer(config.passOptions, primary, global->init) .hasUnremovableSideEffects()) { - primaryUsed.globals.insert(global->name); + ADD_ITEM_TO_TRACKER(globals, global->name, &primaryUsed); } } for (auto& table : primary.tables) { if (table->init && EffectAnalyzer(config.passOptions, primary, table->init) .hasUnremovableSideEffects()) { - primaryUsed.tables.insert(table->name); + ADD_ITEM_TO_TRACKER(tables, table->name, &primaryUsed); } } } - tracker.build(secondaries); - // Scan table initializers into their owning modules. If a table is used by a // single secondary module, its initializer dependencies are marked as "used" // in that secondary module. Otherwise, they are marked as used in the primary @@ -923,7 +913,7 @@ void ModuleSplitter::computeUsedNames() { continue; } if (UsedNames* owner = tracker.getOwner(table->name, tracker.tables)) { - NameCollector(*owner, &tracker).walk(table->init); + NameCollector(*owner, tracker).walk(table->init); } } } @@ -972,7 +962,7 @@ void ModuleSplitter::computeUsedNames() { return false; }; -#define ADD_ITEM_TO_TRACKER(FIELD, VAL) \ +#define ADD_ITEM_TO_TRACKER_TO_TRACKER(FIELD, VAL) \ tracker.insert(VAL, owner, &OwnershipTracker::FIELD, &UsedNames::FIELD) // Iterate on active data and element segments. If its table or memory is @@ -990,10 +980,10 @@ void ModuleSplitter::computeUsedNames() { if (!owner) { return; } - ADD_ITEM_TO_TRACKER(dataSegments, segment->name); - ADD_ITEM_TO_TRACKER(memories, segment->memory); + ADD_ITEM_TO_TRACKER(dataSegments, segment->name, owner); + ADD_ITEM_TO_TRACKER(memories, segment->memory, owner); if (segment->offset) { - NameCollector(*owner, &tracker).walk(segment->offset); + NameCollector(*owner, tracker).walk(segment->offset); } }); @@ -1045,13 +1035,13 @@ void ModuleSplitter::computeUsedNames() { if (!owner) { return; } - ADD_ITEM_TO_TRACKER(elementSegments, segment->name); - ADD_ITEM_TO_TRACKER(tables, segment->table); + ADD_ITEM_TO_TRACKER(elementSegments, segment->name, owner); + ADD_ITEM_TO_TRACKER(tables, segment->table, owner); if (segment->offset) { - NameCollector(*owner, &tracker).walk(segment->offset); + NameCollector(*owner, tracker).walk(segment->offset); } for (auto* item : segment->data) { - NameCollector(*owner, &tracker).walk(item); + NameCollector(*owner, tracker).walk(item); } }); @@ -1063,7 +1053,7 @@ void ModuleSplitter::computeUsedNames() { if (segment->isPassive() && primaryUsed.elementSegments.contains(segment->name)) { for (auto* item : segment->data) { - NameCollector(primaryUsed, &tracker).walk(item); + NameCollector(primaryUsed, tracker).walk(item); } } } @@ -1082,10 +1072,11 @@ void ModuleSplitter::computeUsedNames() { } if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { for (auto* get : FindAll(global->init).list) { - ADD_ITEM_TO_TRACKER(globals, get->name); + ADD_ITEM_TO_TRACKER(globals, get->name, owner); } } } +#undef ADD_ITEM_TO_TRACKER } void ModuleSplitter::shareImportableItems() { From 8912e36883cfffea25f3e17c5f50b50778e41799 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sat, 15 Aug 2026 04:15:48 +0000 Subject: [PATCH 12/14] Use template instead of ADD_ITEM macro --- src/ir/module-splitting.cpp | 66 +++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 6ef3a682d7d..0d11829e034 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -350,11 +350,33 @@ struct OwnershipTracker { using FieldType = std::unordered_set UsedNames::*; using MapType = std::unordered_map OwnershipTracker::*; + template void insert(Name name, UsedNames* owner) { + if constexpr (std::is_same_v) { + insertImpl(name, owner, &OwnershipTracker::tables, &UsedNames::tables); + } else if constexpr (std::is_same_v) { + insertImpl( + name, owner, &OwnershipTracker::memories, &UsedNames::memories); + } else if constexpr (std::is_same_v) { + insertImpl(name, owner, &OwnershipTracker::globals, &UsedNames::globals); + } else if constexpr (std::is_same_v) { + insertImpl(name, owner, &OwnershipTracker::tags, &UsedNames::tags); + } else if constexpr (std::is_same_v) { + insertImpl( + name, owner, &OwnershipTracker::dataSegments, &UsedNames::dataSegments); + } else if constexpr (std::is_same_v) { + insertImpl(name, + owner, + &OwnershipTracker::elementSegments, + &UsedNames::elementSegments); + } + } + // 'mapField' points to one of OwnershipTracker's maps, such as // std::unordered_map globals; // 'field' points to one of UsedName's sets, such as // std::unordered_set globals; - void insert(Name name, UsedNames* owner, MapType mapField, FieldType field) { + void + insertImpl(Name name, UsedNames* owner, MapType mapField, FieldType field) { (owner->*field).insert(name); // Figure out which module is the 'owner' of this item. If it is used by a // single secondary module, that secondary module is the owner. If it is @@ -781,9 +803,6 @@ void ModuleSplitter::computeUsedNames() { UsedNames& primaryUsed = tracker.primaryUsed; std::vector& secondaryUsed = tracker.secondaryUsed; -#define ADD_ITEM_TO_TRACKER(field, val, owner) \ - tracker.insert(val, owner, &OwnershipTracker::field, &UsedNames::field) - struct NameCollector : public PostWalker> { @@ -811,22 +830,22 @@ void ModuleSplitter::computeUsedNames() { if (cast->field.is()) { \ switch (kind) { \ case ModuleItemKind::Table: \ - ADD_ITEM_TO_TRACKER(tables, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::Memory: \ - ADD_ITEM_TO_TRACKER(memories, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::Global: \ - ADD_ITEM_TO_TRACKER(globals, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::Tag: \ - ADD_ITEM_TO_TRACKER(tags, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::DataSegment: \ - ADD_ITEM_TO_TRACKER(dataSegments, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::ElementSegment: \ - ADD_ITEM_TO_TRACKER(elementSegments, cast->field, &used); \ + tracker.insert(cast->field, &used); \ break; \ case ModuleItemKind::Function: \ case ModuleItemKind::Invalid: \ @@ -858,16 +877,16 @@ void ModuleSplitter::computeUsedNames() { for (auto& ex : primary.exports) { switch (ex->kind) { case ExternalKind::Global: - ADD_ITEM_TO_TRACKER(globals, *ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primaryUsed); break; case ExternalKind::Memory: - ADD_ITEM_TO_TRACKER(memories, *ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primaryUsed); break; case ExternalKind::Table: - ADD_ITEM_TO_TRACKER(tables, *ex->getInternalName(), &primaryUsed); + tracker.insert
(*ex->getInternalName(), &primaryUsed); break; case ExternalKind::Tag: - ADD_ITEM_TO_TRACKER(tags, *ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primaryUsed); break; default: break; @@ -877,10 +896,10 @@ void ModuleSplitter::computeUsedNames() { // We need to assume the dispatch table and its base global are used in the // primary module, because we will create segments there later. if (tableManager.dispatchTable) { - ADD_ITEM_TO_TRACKER(tables, tableManager.dispatchTable->name, &primaryUsed); + tracker.insert
(tableManager.dispatchTable->name, &primaryUsed); } if (tableManager.dispatchBase.global) { - ADD_ITEM_TO_TRACKER(globals, tableManager.dispatchBase.global, &primaryUsed); + tracker.insert(tableManager.dispatchBase.global, &primaryUsed); } // If custom-descirptors is enabled, global and table initializers can trap. @@ -891,14 +910,14 @@ void ModuleSplitter::computeUsedNames() { if (global->init && EffectAnalyzer(config.passOptions, primary, global->init) .hasUnremovableSideEffects()) { - ADD_ITEM_TO_TRACKER(globals, global->name, &primaryUsed); + tracker.insert(global->name, &primaryUsed); } } for (auto& table : primary.tables) { if (table->init && EffectAnalyzer(config.passOptions, primary, table->init) .hasUnremovableSideEffects()) { - ADD_ITEM_TO_TRACKER(tables, table->name, &primaryUsed); + tracker.insert
(table->name, &primaryUsed); } } } @@ -980,8 +999,8 @@ void ModuleSplitter::computeUsedNames() { if (!owner) { return; } - ADD_ITEM_TO_TRACKER(dataSegments, segment->name, owner); - ADD_ITEM_TO_TRACKER(memories, segment->memory, owner); + tracker.insert(segment->name, owner); + tracker.insert(segment->memory, owner); if (segment->offset) { NameCollector(*owner, tracker).walk(segment->offset); } @@ -1035,8 +1054,8 @@ void ModuleSplitter::computeUsedNames() { if (!owner) { return; } - ADD_ITEM_TO_TRACKER(elementSegments, segment->name, owner); - ADD_ITEM_TO_TRACKER(tables, segment->table, owner); + tracker.insert(segment->name, owner); + tracker.insert
(segment->table, owner); if (segment->offset) { NameCollector(*owner, tracker).walk(segment->offset); } @@ -1072,11 +1091,10 @@ void ModuleSplitter::computeUsedNames() { } if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { for (auto* get : FindAll(global->init).list) { - ADD_ITEM_TO_TRACKER(globals, get->name, owner); + tracker.insert(get->name, owner); } } } -#undef ADD_ITEM_TO_TRACKER } void ModuleSplitter::shareImportableItems() { From e802edefefb2c92a3d648bddb1412e1330ac7586 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 14 Aug 2026 21:24:28 -0700 Subject: [PATCH 13/14] Update src/ir/module-splitting.cpp Co-authored-by: Thomas Lively --- 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 6315788e7cb..23b10bbc348 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1127,7 +1127,7 @@ void ModuleSplitter::shareImportableItems() { } } primary.removeMemories( - [&](Memory* memory) { return memoriesToRemove.count(memory->name); }); + [&](Memory* memory) { return memoriesToRemove.contains(memory->name); }); std::unordered_set tablesToRemove; for (auto& table : primary.tables) { From ec8c86016546d99d585c43f27b2a03f622a7493b Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 18 Aug 2026 01:52:50 +0000 Subject: [PATCH 14/14] Fix reverted function --- src/ir/module-splitting.cpp | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 2f80b809514..785a84fd43f 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -351,24 +351,19 @@ struct OwnershipTracker { using MapType = std::unordered_map OwnershipTracker::*; template void insert(Name name, UsedNames* owner) { - if constexpr (std::is_same_v) { - insertImpl(name, owner, &OwnershipTracker::tables, &UsedNames::tables); - } else if constexpr (std::is_same_v) { - insertImpl( - name, owner, &OwnershipTracker::memories, &UsedNames::memories); - } else if constexpr (std::is_same_v) { - insertImpl(name, owner, &OwnershipTracker::globals, &UsedNames::globals); - } else if constexpr (std::is_same_v) { - insertImpl(name, owner, &OwnershipTracker::tags, &UsedNames::tags); - } else if constexpr (std::is_same_v) { - insertImpl( - name, owner, &OwnershipTracker::dataSegments, &UsedNames::dataSegments); - } else if constexpr (std::is_same_v) { - insertImpl(name, - owner, - &OwnershipTracker::elementSegments, - &UsedNames::elementSegments); - } +#define INSERT_ITEM(ItemType, field) \ + if constexpr (std::is_same_v) { \ + insertImpl(name, owner, &OwnershipTracker::field, &UsedNames::field); \ + } + + INSERT_ITEM(Table, tables) + else INSERT_ITEM(Memory, memories) else INSERT_ITEM(Global, globals) else INSERT_ITEM( + Tag, + tags) else INSERT_ITEM(DataSegment, + dataSegments) else INSERT_ITEM(ElementSegment, + elementSegments) + +#undef INSERT_ITEM } // 'mapField' points to one of OwnershipTracker's maps, such as