Skip to content

[wasm-split] Precompute ownership info (NFC) - #8986

Open
aheejin wants to merge 7 commits into
mainfrom
wasm_split_owning_modules
Open

[wasm-split] Precompute ownership info (NFC)#8986
aheejin wants to merge 7 commits into
mainfrom
wasm_split_owning_modules

Conversation

@aheejin

@aheejin aheejin commented Aug 11, 2026

Copy link
Copy Markdown
Member

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 #8832 (comment).

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
#8832 (comment).
@aheejin
aheejin requested a review from tlively August 11, 2026 21:00
@aheejin
aheejin requested a review from a team as a code owner August 11, 2026 21:00
aheejin added a commit that referenced this pull request Aug 12, 2026
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.
Comment thread src/ir/module-splitting.cpp Outdated
std::unordered_map<Name, ItemInfo> dataSegments;
std::unordered_map<Name, ItemInfo> elementSegments;

std::unordered_map<UsedNames*, Module*> usedToSecondary;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the need for this map by identifying entries in UsedNames by secondary module index rather than by UsedName*?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it, but not sure this is what you wanted: ba70880

This removes usedToSecondary and figures out which secondary we are referring to by a pointer arithmetic. Did you have something else in mind by "by secondary module index rather than by UsedName*?

Comment thread src/ir/module-splitting.cpp Outdated
// 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];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the relatively new find_or_null utility from src/support/utilities.h here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this map in ba70880.

Comment thread src/ir/module-splitting.cpp Outdated
Comment on lines +372 to +374
if (std::find(vec.begin(), vec.end(), mod) == vec.end()) {
vec.push_back(mod);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to avoid adding the same secondary module to usingSecondaries twice, should usingSecondaries be a set?

@aheejin aheejin Aug 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      for (auto* secondary :                                                     
           tracker.getUsingSecondaries(memory->name, tracker.memories)) {        
        auto* secondaryMemory =                                                  
          ModuleUtils::copyMemory(memory.get(), *secondary);                     
        makeImportExport(                                                        
          *memory, *secondaryMemory, "memory", ExternalKind::Memory);            
      }

We iterate on it in shareImportableItems. The iteration order will be determined by pointer values, so they can be different for each run. I didn't think that loop could produce different results for different iteration order, but I thought it was safer not to do that. Maybe it's fine as long as the code doesn't depend on the iteration order?

We also have InsertOrderedSet, which is a set but also preserves the insertion order. I thought it might be overkill.

I tried both and the running time (3-run average) of acx_gallery is:

  • SmallVector (current): 25.2s
  • SmallSet: 26.3s
  • InsertOrderedSet: 27.1s

Comment thread src/ir/module-splitting.cpp
Comment thread src/ir/module-splitting.cpp Outdated
Comment on lines +796 to +798
// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these two phases relate to the general process of splitting described in the comment at the top of the file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of shareImportableItems (and computeUsedNames called from shareImportableItems and construction of OwnershipTracker inside computeUsedNames) correspond to this paragraph:

//   4. Export globals, tags, tables, and memories from the primary module and
//      import them in the secondary modules. If possible, move those module
//      items instead to the secondary modules.

The reason ADD_ITEM does two different things is, before we create OwnershipTracker, we build UsedName for each module, and used.field.insert(val); is done in this phase. After OwnershipTracker is created, we add more constraints. But unlike the straightforward first scanning phase this constraints can add one module item to multiple modules, and we need to keep track of which one should be the owner and such. So in this second phase we use tracker->insert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants