Skip to content

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

Open
aheejin wants to merge 11 commits into
wasm_split_no_parallelfrom
wasm_split_owning_modules
Open

[wasm-split] Precompute ownership info (NFC)#8986
aheejin wants to merge 11 commits into
wasm_split_no_parallelfrom
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
Comment thread src/ir/module-splitting.cpp Outdated
Comment thread src/ir/module-splitting.cpp Outdated
Comment thread src/ir/module-splitting.cpp
Comment thread src/ir/module-splitting.cpp Outdated
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.

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.

So if I understand correctly, the first pass collects the used names and the second pass computes the owners.

What I'm not understanding yet is why we need two passes for this. Can't we just insert used names into the OwnershipTracker in a single pass and have it automatically update the owner to be the primary module as soon as it sees that two different modules have used the same item?

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.

The reason I used the two-phase thing was the first phase was using ParallelFunctionAnalysis:

// Given a module, collect names used in the module
auto scanModule = [&](Module& module) {
UsedNames used;
ModuleUtils::ParallelFunctionAnalysis<UsedNames> 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);
return used;
};

and if we use the OwnershipTracker from the beginning we can't use ParallelFunctionAnalysis anymore.

But is using ParallelFunctionAnalysis really faster? Several months ago I tried to use ParallelFunctionAnalysis for other functions that were not using it and it actually slowed the program down. I just tried to remove it from scanModule, and it apparently makes at least Dart applications on my local machine faster.

I uploaded the removing of ParallelFunctionAnalysis as a separate PR to make diffs clearer: #9007
After this we can remove this two phase and use the tracker from the beginning.

@aheejin aheejin Aug 15, 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.

Eliminated the two phase and use tracker.insert for all cases, on top of #9007: f03574f

This reduces the running time of acx_gallery from 25s to 23s. (Will update the PR description after running essentials too)

Comment thread src/ir/module-splitting.cpp Outdated
Comment thread src/ir/module-splitting.cpp Outdated
Comment thread src/ir/module-splitting.cpp Outdated
Comment on lines +989 to +990
#define ADD_ITEM_TO_TRACKER(FIELD, VAL) \
tracker.insert(VAL, owner, &OwnershipTracker::FIELD, &UsedNames::FIELD)

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.

This macro implicitly using whatever owner happens to be in scope at the use site makes it harder to understand what's going on. Can we pass the owner in explicitly?

Also it's probably possible to use some template overloading magic to do something like tracker.insert<Memory>(segment->memory, owner) without using macros.

@aheejin aheejin Aug 15, 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.

I tried it. It looks we need an additional template<typename T> insert wrapper method: 8912e36
Do you prefer this?

(And in case you rather prefer the macro, f03574f added the owner explicitly to it)

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

So if I understand correctly, the first pass collects the used names and the second pass computes the owners.

What I'm not understanding yet is why we need two passes for this. Can't we just insert used names into the OwnershipTracker in a single pass and have it automatically update the owner to be the primary module as soon as it sees that two different modules have used the same item?

Co-authored-by: Thomas Lively <tlively123@gmail.com>
@aheejin
aheejin changed the base branch from main to wasm_split_no_parallel August 15, 2026 03:19
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