[wasm-split] Split globals' transitive global dependencies#8442
Merged
[wasm-split] Split globals' transitive global dependencies#8442
Conversation
When splitting a module, if non-function items (memories, tables,
globals, tags) are exclusively used by a single secondary module, this
moves them directly to that secondary module rather than exporting them
from the primary module.
When a global is moved, its initializer can contain `global.get` or
`ref.func`s, creating dependences on other globals and functions. For
now, this PR just exports all the dependences from the primary module to
the secondary module. This will be improved by follow-up PRs.
This PR does not reduce the size of the primary module for acx_gallery
test case; it actually increases it by 2.6%. But this PR is mostly a
preparation for the follow-up PRs, which will reduce the size.
This also sadly increases wasm-split's running time on acx_gallery from
15.7s -> 24.2s, by 54%, due to more computations in
`shareImportableItems`.
---
The below is `wasm-objdump -h` result of the primary modules:
- Before
```
Type start=0x0000000c end=0x00035e09 (size=0x00035dfd) count: 11192
Import start=0x00035e0e end=0x004bd669 (size=0x0048785b) count: 65720
Function start=0x004bd66d end=0x004d0519 (size=0x00012eac) count: 62890
Table start=0x004d051c end=0x004d4059 (size=0x00003b3d) count: 2921
Tag start=0x004d405b end=0x004d405f (size=0x00000004) count: 1
Global start=0x004d4063 end=0x00689ff8 (size=0x001b5f95) count: 80766
Export start=0x00689ffc end=0x0077aafe (size=0x000f0b02) count: 60877
Start start=0x0077ab00 end=0x0077ab02 (size=0x00000002) start: 828
Elem start=0x0077ab06 end=0x007e494b (size=0x00069e45) count: 12303
DataCount start=0x007e494d end=0x007e494e (size=0x00000001) count: 1
Code start=0x007e4953 end=0x00a142ea (size=0x0022f997) count: 62890
Data start=0x00a142ee end=0x00a33e7b (size=0x0001fb8d) count: 1
```
- After (This PR)
```
Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185
Import start=0x00035d49 end=0x003faf6f (size=0x003c5226) count: 56805
Function start=0x003faf73 end=0x0040de1f (size=0x00012eac) count: 62890
Table start=0x0040de22 end=0x0041195d (size=0x00003b3b) count: 2921
Tag start=0x0041195f end=0x00411963 (size=0x00000004) count: 1
Global start=0x00411967 end=0x005541c5 (size=0x0014285e) count: 47771
Export start=0x005541ca end=0x007c0a00 (size=0x0026c836) count: 59077
Start start=0x007c0a02 end=0x007c0a04 (size=0x00000002) start: 828
Elem start=0x007c0a08 end=0x0082a84b (size=0x00069e43) count: 12303
DataCount start=0x0082a84d end=0x0082a84e (size=0x00000001) count: 1
Code start=0x0082a853 end=0x00a5a159 (size=0x0022f906) count: 62890
Data start=0x00a5a15d end=0x00a79cea (size=0x0001fb8d) count: 1
```
Note that even though the size of the global section has decreased by
27% (the number of global by 41%), the size of the export section
increased by 157%, while the number of exports has actually decreased.
The reason is, while we shed some exports for globals, we gained exports
for functions (due to globals `ref.func` dependences), which has a lot
longer names.
Follow-ups:
When a global is exclusively used by a secondary module, we can move it to the secondary module. If its initializer contains a `global.get` of another global, we exported it from the primary module to the secondary module, even if it may not be used anywhere else. When we split a global out to a secondary module, this PR computes the transitive dependency of the split global, and if those globals in the dependency are not used anywhere else in other modules, we move them to the secondary module as well. #8441 and this PR combined reduce the size of the primary module by 29%. The running time of `wasm-split` hasn't really changed with this PR, compared to #8441. --- `wasm-objdump -h` result: - Before (#8441) ``` Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185 Import start=0x00035d49 end=0x003faf6f (size=0x003c5226) count: 56805 Function start=0x003faf73 end=0x0040de1f (size=0x00012eac) count: 62890 Table start=0x0040de22 end=0x0041195d (size=0x00003b3b) count: 2921 Tag start=0x0041195f end=0x00411963 (size=0x00000004) count: 1 Global start=0x00411967 end=0x005541c5 (size=0x0014285e) count: 47771 Export start=0x005541ca end=0x007c0a00 (size=0x0026c836) count: 59077 Start start=0x007c0a02 end=0x007c0a04 (size=0x00000002) start: 828 Elem start=0x007c0a08 end=0x0082a84b (size=0x00069e43) count: 12303 DataCount start=0x0082a84d end=0x0082a84e (size=0x00000001) count: 1 Code start=0x0082a853 end=0x00a5a159 (size=0x0022f906) count: 62890 Data start=0x00a5a15d end=0x00a79cea (size=0x0001fb8d) count: 1 ``` - After (This PR) ``` Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185 Import start=0x00035d48 end=0x00132efc (size=0x000fd1b4) count: 32642 Function start=0x00132f00 end=0x00145dac (size=0x00012eac) count: 62890 Table start=0x00145daf end=0x001498ea (size=0x00003b3b) count: 2921 Tag start=0x001498ec end=0x001498f0 (size=0x00000004) count: 1 Global start=0x001498f4 end=0x00289e60 (size=0x0014056c) count: 47728 Export start=0x00289e65 end=0x004977fe (size=0x0020d999) count: 35861 Start start=0x00497800 end=0x00497802 (size=0x00000002) start: 828 Elem start=0x00497806 end=0x00501649 (size=0x00069e43) count: 12303 DataCount start=0x0050164b end=0x0050164c (size=0x00000001) count: 1 Code start=0x00501651 end=0x00730f22 (size=0x0022f8d1) count: 62890 Data start=0x00730f26 end=0x00750ab3 (size=0x0001fb8d) count: 1 ``` Note that while the decrease in the global section is small, we have a significant size decrease in the import and the export sections, because we used to import globals and export them just to relay those globals to the secondary modules.
aheejin
added a commit
that referenced
this pull request
Mar 10, 2026
When a global is exclusively used by a secondary module and thus moved to that module, and its initializer has a `(ref.func $func)`, we used to create a trampoline and export it from the primary module in all cases, even in the case that the function is in the same secondary module. This now moves those functions referred to by `ref.func`s to the secondary module, as long as they don't have uses anywhere else. To do this, we now skip scanning global initializers in `indirectReferencesToSecondaryFunctions`, and selectively create trampolines only when needed in `shareImportableItems`. The running time of `wasm-split` hasn't really changed with this PR, compared to the previous PR #8442 (~25s range in acx_gallery). #8441, #8442, and this PR combined reduce the size of the primary module by 46.6%. --- `wasm-objdump -h` result: - Before (#8442) ``` Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185 Import start=0x00035d48 end=0x00132efc (size=0x000fd1b4) count: 32642 Function start=0x00132f00 end=0x00145dac (size=0x00012eac) count: 62890 Table start=0x00145daf end=0x001498ea (size=0x00003b3b) count: 2921 Tag start=0x001498ec end=0x001498f0 (size=0x00000004) count: 1 Global start=0x001498f4 end=0x00289e60 (size=0x0014056c) count: 47728 Export start=0x00289e65 end=0x004977fe (size=0x0020d999) count: 35861 Start start=0x00497800 end=0x00497802 (size=0x00000002) start: 828 Elem start=0x00497806 end=0x00501649 (size=0x00069e43) count: 12303 DataCount start=0x0050164b end=0x0050164c (size=0x00000001) count: 1 Code start=0x00501651 end=0x00730f22 (size=0x0022f8d1) count: 62890 Data start=0x00730f26 end=0x00750ab3 (size=0x0001fb8d) count: 1 ``` - After (This PR) ``` Type start=0x0000000c end=0x00035d38 (size=0x00035d2c) count: 11185 Import start=0x00035d3c end=0x00132ef0 (size=0x000fd1b4) count: 32642 Function start=0x00132ef4 end=0x001436cc (size=0x000107d8) count: 53001 Table start=0x001436cf end=0x0014720a (size=0x00003b3b) count: 2921 Tag start=0x0014720c end=0x00147210 (size=0x00000004) count: 1 Global start=0x00147214 end=0x00287b75 (size=0x00140961) count: 47728 Export start=0x00287b79 end=0x002e703f (size=0x0005f4c6) count: 25972 Start start=0x002e7041 end=0x002e7043 (size=0x00000002) start: 828 Elem start=0x002e7047 end=0x00349aa7 (size=0x00062a60) count: 12303 DataCount start=0x00349aa9 end=0x00349aaa (size=0x00000001) count: 1 Code start=0x00349aaf end=0x00550a4e (size=0x00206f9f) count: 53001 Data start=0x00550a52 end=0x005705df (size=0x0001fb8d) count: 1 ``` We can see while the size of the function and the code sections have decreased, the big gains come from the decrease of the export section, which can contain long function names.
This was referenced Mar 10, 2026
Member
Author
|
cc @biggs0125 |
aheejin
added a commit
that referenced
this pull request
Mar 18, 2026
When splitting a module, if non-function items (memories, tables,
globals, tags) are exclusively used by a single secondary module, this
moves them directly to that secondary module rather than exporting them
from the primary module.
When a global is moved, its initializer can contain `global.get` or
`ref.func`s, creating dependences on other globals and functions. For
now, this PR just exports all the dependences from the primary module to
the secondary module. This will be improved by follow-up PRs.
This PR reduces the size of the primary module for acx_gallery by 12.5%.
Follow-up PRs will reduce it further.
This also sadly increases wasm-split's running time on acx_gallery from
16.5s -> 24.7s, by 49%, due to more computations in
`shareImportableItems`.
---
The below is `wasm-objdump -h` result of the primary modules:
- Before
```
Type start=0x0000000c end=0x00035e09 (size=0x00035dfd) count: 11192
Import start=0x00035e0e end=0x004bd669 (size=0x0048785b) count: 65720
Function start=0x004bd66d end=0x004d0519 (size=0x00012eac) count: 62890
Table start=0x004d051c end=0x004d4059 (size=0x00003b3d) count: 2921
Tag start=0x004d405b end=0x004d405f (size=0x00000004) count: 1
Global start=0x004d4063 end=0x00689ff8 (size=0x001b5f95) count: 80766
Export start=0x00689ffc end=0x0071b16c (size=0x00091170) count: 60877
Start start=0x0071b16e end=0x0071b170 (size=0x00000002) start: 828
Elem start=0x0071b174 end=0x00784fb9 (size=0x00069e45) count: 12303
DataCount start=0x00784fbb end=0x00784fbc (size=0x00000001) count: 1
Code start=0x00784fc1 end=0x009b4958 (size=0x0022f997) count: 62890
Data start=0x009b495c end=0x009d44e9 (size=0x0001fb8d) count: 1
```
- After (This PR)
```
Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185
Import start=0x00035d49 end=0x003faf6f (size=0x003c5226) count: 56805
Function start=0x003faf73 end=0x0040de1f (size=0x00012eac) count: 62890
Table start=0x0040de22 end=0x0041195d (size=0x00003b3b) count: 2921
Tag start=0x0041195f end=0x00411963 (size=0x00000004) count: 1
Global start=0x00411967 end=0x005541c5 (size=0x0014285e) count: 47771
Export start=0x005541c9 end=0x005dfc2c (size=0x0008ba63) count: 59077
Start start=0x005dfc2e end=0x005dfc30 (size=0x00000002) start: 828
Elem start=0x005dfc34 end=0x00649a77 (size=0x00069e43) count: 12303
DataCount start=0x00649a79 end=0x00649a7a (size=0x00000001) count: 1
Code start=0x00649a7f end=0x00879385 (size=0x0022f906) count: 62890
Data start=0x00879389 end=0x00898f16 (size=0x0001fb8d) count: 1
```
Follow-ups: #8442 and #8443
tlively
approved these changes
Mar 19, 2026
src/ir/module-splitting.cpp
Outdated
Comment on lines
+999
to
+1003
| // We shouldn't use collector.walkModuleCode here, because we don't want to | ||
| // walk on global initializers. At this point, all globals are still in the | ||
| // primary module, so if we walk on global initializers here, globals appear | ||
| // in their initialalizers will be all marked as used in the primary module, | ||
| // which is not true. |
Member
There was a problem hiding this comment.
Suggested change
| // We shouldn't use collector.walkModuleCode here, because we don't want to | |
| // walk on global initializers. At this point, all globals are still in the | |
| // primary module, so if we walk on global initializers here, globals appear | |
| // in their initialalizers will be all marked as used in the primary module, | |
| // which is not true. | |
| // We shouldn't use collector.walkModuleCode here, because we don't want to | |
| // walk global initializers. At this point, all globals are still in the | |
| // primary module, so if we walk global initializers here, globals used | |
| // in other globals' initialalizers will all be marked as used in the primary module, | |
| // which is not true. |
Member
Author
There was a problem hiding this comment.
This has some conflicting suggestions with #8442 (comment). I followed #8442 (comment)
Member
There was a problem hiding this comment.
It would be good to add another similar test where there is another use of $b or $c that prevents it from being moved.
Co-authored-by: Thomas Lively <tlively123@gmail.com>
aheejin
commented
Mar 19, 2026
| (module | ||
| ;; PRIMARY: (global $c i32 (i32.const 42)) | ||
| ;; SECONDARY: (global $c i32 (i32.const 42)) | ||
| (global $c i32 (i32.const 42)) |
Member
Author
There was a problem hiding this comment.
By the way what's the indentation rule for new test files now? One space or two spaces?
Member
There was a problem hiding this comment.
We're not totally consistent, but I generally prefer two spaces
Member
|
@aheejin the fuzzer is hitting an assert after this landed: (module
(rec
(type $2 (sub (func (param i32 i64))))
(type $3 (sub $2 (func (param i32 i64))))
(type $6 (func (param i64)))
(type $0 (func))
(type $1 (sub (func (result (ref $1)))))
(type $5 (array (mut externref)))
)
(import "__fuzz_import" "extern$" (global $gimport$0 externref))
(import "__fuzz_import" "extern$_18" (global $gimport$1 (ref extern)))
(global $global$0 (mut (ref $5)) (array.new $5
(global.get $gimport$0)
(i32.const 0)
))
(global $global$1 (ref $1) (ref.func $2))
(global $global$2 funcref (ref.func $0))
(global $global$3 f32 (f32.const 0))
(global $global$4 f64 (f64.const 0))
(global $global$5 v128 (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000))
(global $global$6 (ref eq) (ref.i31
(i32.const 0)
))
(global $global$7 (mut funcref) (global.get $global$2))
(global $global$8 (ref $0) (ref.func $0))
(global $global$9 (ref null $2) (ref.null nofunc))
(global $global$10 (ref null $2) (ref.func $1))
(global $global$11 i64 (i64.const 0))
(global $global$12 i64 (i64.const 0))
(global $global$13 i64 (i64.const 0))
(global $global$14 i64 (i64.const 0))
(global $global$15 (ref null $2) (ref.null nofunc))
(global $global$16 f64 (f64.const 0))
(global $global$17 (mut i32) (i32.const 0))
(memory $0 16 17 shared)
(table $0 5 funcref)
(table $1 2 exnref)
(elem $0 externref)
(elem $1 func)
(elem $2 (table $0) (i32.const 0) func)
(tag $tag$0 (type $6) (param i64))
(tag $tag$1 (type $0))
(tag $tag$2 (type $0))
(func $0 (type $0)
(nop)
)
(func $1 (type $3) (param $0 i32) (param $1 i64)
(nop)
)
(func $2 (type $1) (result (ref $1))
(unreachable)
)
(func $10 (type $0)
(drop
(global.get $global$2)
)
)
) |
aheejin
added a commit
that referenced
this pull request
Mar 21, 2026
…#8443) When a global is exclusively used by a secondary module and thus moved to that module, and its initializer has a `(ref.func $func)`, we used to create a trampoline and export it from the primary module in all cases, even in the case that the function is in the same secondary module. This now only avoids creating a trampoline when the function is already in the same secondary module. To do this, we now skip scanning global initializers in `indirectReferencesToSecondaryFunctions`, and selectively create trampolines only when needed in `shareImportableItems`. The running time of `wasm-split` hasn't really changed with this PR, compared to the previous PR #8442 (~25s range in acx_gallery). #8441, #8442, and this PR combined reduce the size of the primary module of acx_gallery by 45.4%. --- `wasm-objdump -h` result: - Before (#8442) ``` Type start=0x0000000c end=0x00035d44 (size=0x00035d38) count: 11185 Import start=0x00035d48 end=0x00132efc (size=0x000fd1b4) count: 32642 Function start=0x00132f00 end=0x00145dac (size=0x00012eac) count: 62890 Table start=0x00145daf end=0x001498ea (size=0x00003b3b) count: 2921 Tag start=0x001498ec end=0x001498f0 (size=0x00000004) count: 1 Global start=0x001498f4 end=0x00289e60 (size=0x0014056c) count: 47728 Export start=0x00289e64 end=0x002e99c1 (size=0x0005fb5d) count: 35861 Start start=0x002e99c3 end=0x002e99c5 (size=0x00000002) start: 828 Elem start=0x002e99c9 end=0x0035380c (size=0x00069e43) count: 12303 DataCount start=0x0035380e end=0x0035380f (size=0x00000001) count: 1 Code start=0x00353814 end=0x005830e5 (size=0x0022f8d1) count: 62890 Data start=0x005830e9 end=0x005a2c76 (size=0x0001fb8d) count: 1 ``` - After (This PR) ``` Type start=0x0000000c end=0x00035d38 (size=0x00035d2c) count: 11185 Import start=0x00035d3c end=0x00132ef0 (size=0x000fd1b4) count: 32642 Function start=0x00132ef4 end=0x001436cc (size=0x000107d8) count: 53001 Table start=0x001436cf end=0x0014720a (size=0x00003b3b) count: 2921 Tag start=0x0014720c end=0x00147210 (size=0x00000004) count: 1 Global start=0x00147214 end=0x00287b75 (size=0x00140961) count: 47728 Export start=0x00287b79 end=0x002d41ce (size=0x0004c655) count: 25972 Start start=0x002d41d0 end=0x002d41d2 (size=0x00000002) start: 828 Elem start=0x002d41d6 end=0x00336c36 (size=0x00062a60) count: 12303 DataCount start=0x00336c38 end=0x00336c39 (size=0x00000001) count: 1 Code start=0x00336c3e end=0x0053dbdd (size=0x00206f9f) count: 53001 Data start=0x0053dbe1 end=0x0055d76e (size=0x0001fb8d) count: 1 ```
aheejin
added a commit
to aheejin/binaryen
that referenced
this pull request
Mar 21, 2026
Currently dead module items are not split and just end up remaining in the primary module. Usually a user runs DCE before or after the splitting, and the goal of wasm-split is not DCE, so from the optimization perspective it shouldn't be a problem. But after WebAssembly#8441, this can be a problem because a dead global's initializer can refer to another global that is moved to a secondary module: ```wast ;; Primary (global.get $dead i32 (global.get $a)) ;; Secondary (global $a i32 (...)) ``` This PR just removes those dead globals. We leave it and do some post processing to make it work but that's more complicated, or we can move it to the same secondary module but this requires scanning of the reverse mapping. Removing it seems the simplest. We can remove those dead items for other module items (memories, tables, and tags) but they are not necessary for wasm-split to run, and they can be handled later in DCE. Fixes WebAssembly#8442 (comment).
Member
Author
aheejin
added a commit
that referenced
this pull request
Mar 21, 2026
Currently dead module items are not split and just end up remaining in the primary module. Usually a user runs DCE before or after the splitting, and the goal of wasm-split is not DCE, so from the optimization perspective it shouldn't be a problem. But after #8441, this can be a problem because a dead global's initializer can refer to another global that is moved to a secondary module: ```wast ;; Primary (global.get $dead i32 (global.get $a)) ;; Secondary (global $a i32 (...)) ``` This PR just removes those dead globals. We leave it and do some post processing to make it work but that's more complicated, or we can move it to the same secondary module but this requires scanning of the reverse mapping. Removing it seems the simplest. We can remove dead items for other module items (memories, tables, and tags) but it is not necessary for wasm-split to run, and they can be handled later in DCE. Fixes #8442 (comment).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a global is exclusively used by a secondary module, we can move it to the secondary module. If its initializer contains a
global.getof another global, we exported it from the primary module to the secondary module, even if it may not be used anywhere else.When we split a global out to a secondary module, this PR computes the transitive dependency of the split global, and if those globals in the dependency are not used anywhere else in other modules, we move them to the secondary module as well.
#8441 and this PR combined reduce the size of the primary module of acx_gallery by 42.6%.
The running time of
wasm-splithasn't really changed with this PR, compared to #8441.wasm-objdump -hresult:Note that while the decrease in the global section is small, we have a significant size decrease in the import and the export sections, because we used to import globals and export them just to relay those globals to the secondary modules.
Follow-up: #8443
Fixes #7724.