[wasm-split] Split immutable globals - #9064
Conversation
Previously, we treated all modules items in the same way: if a module
item was used in the primary module or multiple secondary modules, we
put the item in the primary module and exported it from there, and
secondary modules using the item imported it. So
1. When an item was used in both the primary and secondary modules, it
was placed in the primary module and exported from there, and
imported from the secondary modules
2. When an item was not used in the primary module but used in multiple
secondary modules, it was still placed in the primary module and
exported from there, and imported from the secondary modules
But in case of immutable globals, we can have as many copies of the same
global as possible, and we can just copy them to the secondary modules.
So whether it is used in the primary module or multiple secondary
modules, each module that uses it has its own copy. This reduces the
primary module size because, in case of 1, we don't need to put the
global in the primary module in the first place. Also in case of 2, we
don't need to add an export of a global to the primary module.
This is a trade-off and may increase the secondary module sizes instead.
But given that the main goal of using wasm-split is mostly to reduce the
loading time of the primary module, I think this trade-off is worth it.
In case of merge-opt-split workflow, after interprocedural
optimizations, many globals that used to be only used in a single
secondary module gets used in multiple secondary modules, and thus are
placed in the primary module after splitting, increasing the primary
module size. This undoes this effect.
Note that this also copy immutable global imports to secondary modules
whenever it is possible, because we didn't exclude them in
`shareElement`.
---
Results
- acx_gallery
- Input
- Total: 23648k
- Primary: 1337k
- Current
- Total: 22240k
- Primary: 1579k
- This PR
- Total: 22884k
- vs. Current: +2.8%
- vs. Input: -3.2%
- Primary: 1348k
- vs. Current: -14.6%
- vs. Input: +0.8%
- essentials
- Input
- Total: 83384k
- Primary: 5600k
- Current
- Total: 77060k
- Primary: 6396k
- This PR
- Total: 79624k
- vs. Current: +3.3%
- vs. Input: -4.5%
- Primary: 5715k
- vs. Current: -10.6%
- vs. Input: +2.0%
Compared to the current code (without this patch), we can see this
reduces the primary module size significantly (-14.6% and -10.6% each)
at the expense of slight increase of the total combined module size
(+3-4%).
But compared to the original input, this still does not reduce the
primary module size (+1-2%), while it decreases the combined
module size slightly (-3-4%).
The measurements were done with with 07/2026 version of Dart apps, and
after removing all (internal) exports that were not marked as
`@binaryen.js.called`. (I have a pass that does this in my local
machine, but I haven't uploaded it yet)
There was a problem hiding this comment.
We need to make sure we don't copy immutable globals whose initializers contain Properties::isGenerative expressions (such as struct.new) because there would be an observable difference (e.g. struct identity tested with ref.eq) between the values in the copies.
| // In case of mutable globals, we cannot have multiple copies. Compute the | ||
| // 'owner' of the global and insert its dependent globals there. | ||
| if (global->mutable_) { | ||
| if (UsedNames* owner = tracker.getOwner(global->name, tracker.globals)) { |
There was a problem hiding this comment.
It would be nice if we could have tracker.getOwner<Global>(global->name). This might be possible using struct specialization to create a type-level mapping between module item types (Global, Memory, etc) and OwnershipTracker member pointers. (This mechanism could also potentially replace the macros in OwnershipTracker::insert.)
| for (size_t i = 0; i < secondaries.size(); ++i) { | ||
| Module* sec = secondaries[i].get(); | ||
| if (std::find(usingSecs.begin(), usingSecs.end(), sec) != | ||
| usingSecs.end()) { |
There was a problem hiding this comment.
This looks like it's doing "for each secondary module mi, if usingSecs contains mi, then mark all the referenced globals as used in mi." Why do we need the std::find here? Can we not just do something like this?
for i in usingSecs:
for get in gets:
tracker.insert(get->name, &secondaryUsed[i]);
Previously, we treated all modules items in the same way: if a module item was used in the primary module or multiple secondary modules, we put the item in the primary module and exported it from there, and secondary modules using the item imported it. So
But in case of immutable globals, we can have as many copies of the same global as possible, and we can just copy them to the secondary modules. So whether it is used in the primary module or multiple secondary modules, each module that uses it has its own copy. This reduces the primary module size because, in case of 1, we don't need to put the global in the primary module in the first place. Also in case of 2, we don't need to add an export of a global to the primary module.
This is a trade-off and may increase the secondary module sizes instead. But given that the main goal of using wasm-split is mostly to reduce the loading time of the primary module, I think this trade-off is worth it.
In case of merge-opt-split workflow, after interprocedural optimizations, many globals that used to be only used in a single secondary module gets used in multiple secondary modules, and thus are placed in the primary module after splitting, increasing the primary module size. This undoes this effect.
Note that this also copy immutable global imports to secondary modules whenever it is possible, because we didn't exclude them in
shareElement.Results
acx_gallery
essentials
Compared to the current code (without this patch), we can see this reduces the primary module size significantly (-14.6% and -10.6% each) at the expense of slight increase of the total combined module size (+3-4%).
But compared to the original input, this still does not reduce the primary module size (+1-2%), while it decreases the combined module size slightly (-3-4%).
The measurements were done with with 07/2026 version of Dart apps, and after removing all (internal) exports that were not marked as
@binaryen.js.called. (I have a pass that does this in my local machine, but I haven't uploaded it yet)