From f5b8667cdf056187e901a3a7655ba1aefd0d0bca Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 9 Nov 2026 07:37:05 +0000 Subject: [PATCH 1/3] [wasm-split] Simplify OwnershipTracker (NFC) This removes `UsedNames` and directly uses `Module`s in place of that. `UsedNames` was first added in #8221, and after #8986, it becamse largely unnecessary and made code more complicated. Also this removes `owner` field from `ItemInfo`, which can be computed. This also templatize member functions in `OwnershipTracker` so they can be called with a simpler syntax of `tracker.func(name)`. --- src/ir/module-splitting.cpp | 245 +++++++++++++++--------------------- 1 file changed, 103 insertions(+), 142 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index fb682178b39..52b4cee53d2 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -317,25 +317,15 @@ TableSlotManager::Slot TableSlotManager::getSlot(Name func, HeapType type) { // 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; + Module& primary; + + OwnershipTracker(Module& primary) : primary(primary) {} struct ItemInfo { - UsedNames* owner = nullptr; SmallVector usingSecondaries; bool usedByPrimary = false; }; @@ -347,75 +337,65 @@ struct OwnershipTracker { std::unordered_map dataSegments; std::unordered_map elementSegments; - const std::vector>* secondaries = nullptr; - - using FieldType = std::unordered_set UsedNames::*; - using MapType = std::unordered_map OwnershipTracker::*; - - template void insert(Name name, UsedNames* owner) { -#define INSERT_ITEM(ItemType, field) \ - if constexpr (std::is_same_v) { \ - insertImpl(name, owner, &OwnershipTracker::field, &UsedNames::field); \ + template std::unordered_map& getMap() { + if constexpr (std::is_same_v) { + return tables; + } else if constexpr (std::is_same_v) { + return memories; + } else if constexpr (std::is_same_v) { + return globals; + } else if constexpr (std::is_same_v) { + return tags; + } else if constexpr (std::is_same_v) { + return dataSegments; + } else if constexpr (std::is_same_v) { + return elementSegments; + } else { + static_assert(!sizeof(T*), "Unsupported module item type"); + } } - INSERT_ITEM(Table, tables) - INSERT_ITEM(Memory, memories) - INSERT_ITEM(Global, globals) - INSERT_ITEM(Tag, tags) - INSERT_ITEM(DataSegment, dataSegments) - INSERT_ITEM(ElementSegment, elementSegments) -#undef INSERT_ITEM + + template void insert(Name name, Module* module) { + insertImpl(name, module, getMap()); } - // '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 - 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 - // 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(); - secondary = (*secondaries)[index].get(); - } - if (inserted) { - if (secondary) { - it->second.usingSecondaries.push_back(secondary); - } + void insertImpl(Name name, + Module* module, + std::unordered_map& map) { + auto& info = map[name]; + if (module == &primary) { + info.usedByPrimary = true; } else { - if (it->second.owner != owner) { - it->second.owner = &primaryUsed; - (primaryUsed.*field).insert(name); - } - if (secondary) { - auto& vec = it->second.usingSecondaries; - if (std::find(vec.begin(), vec.end(), secondary) == vec.end()) { - vec.push_back(secondary); - } + auto& vec = info.usingSecondaries; + if (std::find(vec.begin(), vec.end(), module) == vec.end()) { + vec.push_back(module); } } } - UsedNames* getOwner(Name name, - const std::unordered_map& map) { + // 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. + template Module* getOwner(Name name) { + auto& map = getMap(); auto it = map.find(name); - if (it != map.end()) { - return it->second.owner; + if (it == map.end()) { + return nullptr; + } + const auto& info = it->second; + if (info.usedByPrimary || info.usingSecondaries.size() > 1) { + return &primary; + } + if (info.usingSecondaries.size() == 1) { + return info.usingSecondaries[0]; } return nullptr; } - const SmallVector& - getUsingSecondaries(Name name, - const std::unordered_map& map) { + template + const SmallVector& getUsingSecondaries(Name name) { + auto& map = getMap(); auto it = map.find(name); if (it != map.end()) { return it->second.usingSecondaries; @@ -424,7 +404,8 @@ struct OwnershipTracker { return empty; } - bool isUnused(Name name, const std::unordered_map& map) { + template bool isUnused(Name name) { + auto& map = getMap(); auto it = map.find(name); if (it != map.end()) { return !it->second.usedByPrimary && it->second.usingSecondaries.empty(); @@ -432,8 +413,8 @@ struct OwnershipTracker { return true; } - bool isUsedByPrimary(Name name, - const std::unordered_map& map) { + template bool isUsedByPrimary(Name name) { + auto& map = getMap(); auto it = map.find(name); if (it != map.end()) { return it->second.usedByPrimary; @@ -441,8 +422,8 @@ struct OwnershipTracker { return false; } - bool usedBySingleSecondary(Name name, - const std::unordered_map& map) { + template bool usedBySingleSecondary(Name name) { + auto& map = getMap(); auto it = map.find(name); if (it != map.end()) { return !it->second.usedByPrimary && @@ -500,7 +481,7 @@ struct ModuleSplitter { OwnershipTracker tracker; - void computeUsedNames(); + void computeUsedItems(); // Main splitting steps void classifyFunctions(); @@ -515,7 +496,8 @@ struct ModuleSplitter { ModuleSplitter(Module& primary, const Config& config) : config(config), primary(primary), tableManager(primary, secondaries), exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)), - exportedPrimaryItems(initExportedPrimaryItems(primary)) { + exportedPrimaryItems(initExportedPrimaryItems(primary)), + tracker(primary) { classifyFunctions(); moveSecondaryFunctions(); thunkExportedSecondaryFunctions(); @@ -791,20 +773,15 @@ static bool canCopyGlobal(Global* global, Module& wasm) { !Properties::isGenerative(global->init, nullptr, wasm)); } -void ModuleSplitter::computeUsedNames() { - tracker.secondaries = &secondaries; - tracker.secondaryUsed.resize(secondaries.size()); - UsedNames& primaryUsed = tracker.primaryUsed; - std::vector& secondaryUsed = tracker.secondaryUsed; - +void ModuleSplitter::computeUsedItems() { struct NameCollector : public PostWalker> { - UsedNames& used; + Module& module; OwnershipTracker& tracker; - NameCollector(UsedNames& used, OwnershipTracker& tracker) - : used(used), tracker(tracker) {} + NameCollector(Module& module, OwnershipTracker& tracker) + : module(module), tracker(tracker) {} void visitExpression(Expression* curr) { #define DELEGATE_ID curr->_id @@ -824,22 +801,22 @@ void ModuleSplitter::computeUsedNames() { if (cast->field.is()) { \ switch (kind) { \ case ModuleItemKind::Table: \ - tracker.insert(cast->field, &used); \ + tracker.insert
(cast->field, &module); \ break; \ case ModuleItemKind::Memory: \ - tracker.insert(cast->field, &used); \ + tracker.insert(cast->field, &module); \ break; \ case ModuleItemKind::Global: \ - tracker.insert(cast->field, &used); \ + tracker.insert(cast->field, &module); \ break; \ case ModuleItemKind::Tag: \ - tracker.insert(cast->field, &used); \ + tracker.insert(cast->field, &module); \ break; \ case ModuleItemKind::DataSegment: \ - tracker.insert(cast->field, &used); \ + tracker.insert(cast->field, &module); \ break; \ case ModuleItemKind::ElementSegment: \ - tracker.insert(cast->field, &used); \ + tracker.insert(cast->field, &module); \ break; \ case ModuleItemKind::Function: \ case ModuleItemKind::Invalid: \ @@ -852,8 +829,8 @@ void ModuleSplitter::computeUsedNames() { }; // Given a module, collect names used in the module - auto scanModule = [&](Module& module, UsedNames& used) { - NameCollector collector(used, tracker); + auto scanModule = [&](Module& module) { + NameCollector collector(module, tracker); for (auto& func : module.functions) { if (!func->imported()) { collector.walk(func->body); @@ -861,9 +838,9 @@ void ModuleSplitter::computeUsedNames() { } }; - scanModule(primary, primaryUsed); - for (size_t i = 0; i < secondaries.size(); ++i) { - scanModule(*secondaries[i], secondaryUsed[i]); + scanModule(primary); + for (auto& secondary : secondaries) { + scanModule(*secondary); } // If primary module has exports, they are "used" in it. Secondary modules @@ -871,16 +848,16 @@ void ModuleSplitter::computeUsedNames() { for (auto& ex : primary.exports) { switch (ex->kind) { case ExternalKind::Global: - tracker.insert(*ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primary); break; case ExternalKind::Memory: - tracker.insert(*ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primary); break; case ExternalKind::Table: - tracker.insert
(*ex->getInternalName(), &primaryUsed); + tracker.insert
(*ex->getInternalName(), &primary); break; case ExternalKind::Tag: - tracker.insert(*ex->getInternalName(), &primaryUsed); + tracker.insert(*ex->getInternalName(), &primary); break; default: break; @@ -890,10 +867,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) { - tracker.insert
(tableManager.dispatchTable->name, &primaryUsed); + tracker.insert
(tableManager.dispatchTable->name, &primary); } if (tableManager.dispatchBase.global) { - tracker.insert(tableManager.dispatchBase.global, &primaryUsed); + tracker.insert(tableManager.dispatchBase.global, &primary); } // If custom-descirptors is enabled, global and table initializers can trap. @@ -904,14 +881,14 @@ void ModuleSplitter::computeUsedNames() { if (global->init && EffectAnalyzer(config.passOptions, primary, global->init) .hasUnremovableSideEffects()) { - tracker.insert(global->name, &primaryUsed); + tracker.insert(global->name, &primary); } } for (auto& table : primary.tables) { if (table->init && EffectAnalyzer(config.passOptions, primary, table->init) .hasUnremovableSideEffects()) { - tracker.insert
(table->name, &primaryUsed); + tracker.insert
(table->name, &primary); } } } @@ -925,7 +902,7 @@ void ModuleSplitter::computeUsedNames() { if (!table->init) { continue; } - if (UsedNames* owner = tracker.getOwner(table->name, tracker.tables)) { + if (Module* owner = tracker.getOwner
(table->name)) { NameCollector(*owner, tracker).walk(table->init); } } @@ -981,11 +958,11 @@ void ModuleSplitter::computeUsedNames() { // 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 = tracker.getOwner(segment->memory, tracker.memories); + Module* owner = tracker.getOwner(segment->memory); // Trapping segments should be kept in the primary module because they are // evaluated at the instantiation time. if (mayTrap(segment)) { - owner = &primaryUsed; + owner = &primary; } if (!owner) { return; @@ -998,14 +975,14 @@ void ModuleSplitter::computeUsedNames() { }); ModuleUtils::iterActiveElementSegments(primary, [&](ElementSegment* segment) { - UsedNames* owner = tracker.getOwner(segment->table, tracker.tables); + Module* owner = tracker.getOwner
(segment->table); // 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 // secondary module, because those functions aren't available until the // secondary module is loaded anyway. if (!config.usePlaceholders && segment->type.isFunction() && - owner == &primaryUsed) { + owner == &primary) { bool foundSecondary = false; Index secondaryIndex = 0; bool keepInPrimary = false; @@ -1035,12 +1012,12 @@ void ModuleSplitter::computeUsedNames() { } } if (!keepInPrimary && foundSecondary) { - owner = &secondaryUsed[secondaryIndex]; + owner = secondaries[secondaryIndex].get(); } } if (mayTrap(segment)) { - owner = &primaryUsed; + owner = &primary; } if (!owner) { return; @@ -1061,9 +1038,9 @@ void ModuleSplitter::computeUsedNames() { // to the primary module. for (auto& segment : primary.elementSegments) { if (segment->isPassive() && - primaryUsed.elementSegments.contains(segment->name)) { + tracker.getOwner(segment->name) == &primary) { for (auto* item : segment->data) { - NameCollector(primaryUsed, tracker).walk(item); + NameCollector(primary, tracker).walk(item); } } } @@ -1089,7 +1066,7 @@ void ModuleSplitter::computeUsedNames() { // 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)) { + if (Module* owner = tracker.getOwner(global->name)) { for (auto* get : gets) { tracker.insert(get->name, owner); } @@ -1101,27 +1078,21 @@ void ModuleSplitter::computeUsedNames() { // 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)) { + if (tracker.isUsedByPrimary(global->name)) { for (auto* get : gets) { - tracker.insert(get->name, &primaryUsed); + tracker.insert(get->name, &primary); } } - 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]); - } + for (Module* sec : tracker.getUsingSecondaries(global->name)) { + for (auto* get : gets) { + tracker.insert(get->name, sec); } } } } void ModuleSplitter::shareImportableItems() { - computeUsedNames(); + computeUsedItems(); // Share module items with secondary modules. // 1. Only share an item with the modules that use it @@ -1131,7 +1102,6 @@ void ModuleSplitter::shareImportableItems() { // 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, auto removeElements, const char* exportName = nullptr, @@ -1144,15 +1114,13 @@ void ModuleSplitter::shareImportableItems() { isCopyableGlobal = canCopyGlobal(element.get(), primary); } - if (tracker.isUnused(element->name, trackerElements)) { + if (tracker.isUnused(element->name)) { // If this element is not used anywhere, just remove it elementsToRemove.insert(element->name); - } else if (tracker.usedBySingleSecondary(element->name, - trackerElements)) { + } else if (tracker.usedBySingleSecondary(element->name)) { // If this element is used in a single secondary module, move it to the // secondary module - auto* secondary = - tracker.getUsingSecondaries(element->name, trackerElements)[0]; + auto* secondary = tracker.getUsingSecondaries(element->name)[0]; copyElement(element.get(), *secondary); elementsToRemove.insert(element->name); } else if (isCopyableGlobal) { @@ -1160,11 +1128,10 @@ void ModuleSplitter::shareImportableItems() { // 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)) { + if (!tracker.isUsedByPrimary(element->name)) { elementsToRemove.insert(element->name); } - for (auto* secondary : - tracker.getUsingSecondaries(element->name, trackerElements)) { + for (auto* secondary : tracker.getUsingSecondaries(element->name)) { copyElement(element.get(), *secondary); } } else { @@ -1175,7 +1142,7 @@ void ModuleSplitter::shareImportableItems() { // segments. if constexpr (std::is_base_of_v) { for (auto* secondary : - tracker.getUsingSecondaries(element->name, trackerElements)) { + tracker.getUsingSecondaries(element->name)) { auto* secondaryElement = copyElement(element.get(), *secondary); makeImportExport(*element, *secondaryElement, exportName, kind); } @@ -1187,14 +1154,12 @@ void ModuleSplitter::shareImportableItems() { }; shareElements(primary.memories, - tracker.memories, ModuleUtils::copyMemory, &Module::removeMemories, "memory", ExternalKind::Memory); shareElements(primary.tables, - tracker.tables, ModuleUtils::copyTable, &Module::removeTables, "table", @@ -1207,14 +1172,12 @@ void ModuleSplitter::shareImportableItems() { } } shareElements(primary.globals, - tracker.globals, ModuleUtils::copyGlobal, &Module::removeGlobals, "global", ExternalKind::Global); shareElements(primary.tags, - tracker.tags, ModuleUtils::copyTag, &Module::removeTags, "tag", @@ -1225,12 +1188,10 @@ void ModuleSplitter::shareImportableItems() { // indirectReferencesToSecondaryFunctions.) shareElements(primary.dataSegments, - tracker.dataSegments, ModuleUtils::copyDataSegment, &Module::removeDataSegments); shareElements(primary.elementSegments, - tracker.elementSegments, ModuleUtils::copyElementSegment, &Module::removeElementSegments); } From cbe2bed523cebe0924a770db166902ce4faf8d98 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 9 Nov 2026 10:20:22 +0000 Subject: [PATCH 2/3] Simplify getOwner and isUnused --- src/ir/module-splitting.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 52b4cee53d2..0c02c1f2f2d 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -387,10 +387,8 @@ struct OwnershipTracker { if (info.usedByPrimary || info.usingSecondaries.size() > 1) { return &primary; } - if (info.usingSecondaries.size() == 1) { - return info.usingSecondaries[0]; - } - return nullptr; + assert(info.usingSecondaries.size() == 1); + return info.usingSecondaries[0]; } template @@ -405,12 +403,7 @@ struct OwnershipTracker { } template bool isUnused(Name name) { - auto& map = getMap(); - auto it = map.find(name); - if (it != map.end()) { - return !it->second.usedByPrimary && it->second.usingSecondaries.empty(); - } - return true; + return !getMap().contains(name); } template bool isUsedByPrimary(Name name) { From 0785c49c391ea96bd5cb483809f727dec157b570 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 10 Nov 2026 03:26:02 +0000 Subject: [PATCH 3/3] Don't use static_assert(!sizeof(T*)) --- src/ir/module-splitting.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 0c02c1f2f2d..d4eb1bd8af9 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -348,10 +348,10 @@ struct OwnershipTracker { return tags; } else if constexpr (std::is_same_v) { return dataSegments; - } else if constexpr (std::is_same_v) { - return elementSegments; } else { - static_assert(!sizeof(T*), "Unsupported module item type"); + static_assert(std::is_same_v, + "Unsupported module item type"); + return elementSegments; } }