From cfd5b0cd2943efb72728f619f31e3bb268b86475 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 17 Jun 2026 07:41:49 +0000 Subject: [PATCH 1/5] [wasm-split] Split more active segments for --no-placeholders 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. The primary module size decreases 5-9% for acx_gallery and essentials. These applications both use `--no-placeholders`. - acx_gallery (07/2025): 9.7% - acx_gallery (05/2026): 5.3% - essentials (04?/2026): 6.3% - essentials (05/2026): 8.4% --- src/ir/module-splitting.cpp | 35 +++++++ .../multi-split-elems-no-placeholders.wast | 50 ++++++++++ .../split-elems-no-placeholders.wast | 94 +++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 test/lit/wasm-split/multi-split-elems-no-placeholders.wast create mode 100644 test/lit/wasm-split/split-elems-no-placeholders.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 7acbf1000d6..d3af7fa937c 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -871,6 +871,8 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { // primary module and scan it there. ModuleUtils::iterActiveDataSegments(primary, [&](DataSegment* segment) { UsedNames* owner = getOwner(segment->memory, &UsedNames::memories); + // Trapping segments should be kept in the primary module because they are + // evaluated at the instantiation time. if (mayTrap(segment)) { owner = &primaryUsed; } @@ -886,6 +888,39 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { ModuleUtils::iterActiveElementSegments(primary, [&](ElementSegment* segment) { UsedNames* owner = getOwner(segment->table, &UsedNames::tables); + + // 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) { + bool foundSecondary = false; + Index secondaryIndex = 0; + bool keepInPrimary = false; + for (auto* item : segment->data) { + if (item->is()) { + keepInPrimary = true; + break; + } else if (auto* ref = item->dynCast()) { + auto it = funcToSecondaryIndex.find(ref->func); + if (it == funcToSecondaryIndex.end()) { + keepInPrimary = true; + break; + } + if (foundSecondary && secondaryIndex != it->second) { + keepInPrimary = true; + break; + } + foundSecondary = true; + secondaryIndex = it->second; + } + } + if (!keepInPrimary && foundSecondary) { + owner = &secondaryUsed[secondaryIndex]; + } + } + if (mayTrap(segment)) { owner = &primaryUsed; } diff --git a/test/lit/wasm-split/multi-split-elems-no-placeholders.wast b/test/lit/wasm-split/multi-split-elems-no-placeholders.wast new file mode 100644 index 00000000000..8748d1dced6 --- /dev/null +++ b/test/lit/wasm-split/multi-split-elems-no-placeholders.wast @@ -0,0 +1,50 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-split -all -g --multi-split %s --no-placeholders --manifest %S/multi-split.wast.manifest --out-prefix=%t -o %t.wasm +;; RUN: wasm-dis %t.wasm | filecheck %s --check-prefix=PRIMARY +;; RUN: wasm-dis %t1.wasm | filecheck %s --check-prefix=MOD1 +;; RUN: wasm-dis %t2.wasm | filecheck %s --check-prefix=MOD2 +;; RUN: wasm-dis %t3.wasm | filecheck %s --check-prefix=MOD3 + +;; When placeholders are NOT used and all functions in an element segment belong +;; to a single secondary module, we can move the segment to that secondary +;; module. + +(module + ;; PRIMARY: (table $table 3 3 funcref) + (table $table 3 3 funcref) + ;; PRIMARY: (elem $primary-elem1 (table $table) (i32.const 0) func $trampoline_A $trampoline_B $trampoline_C) + + ;; PRIMARY: (elem $primary-elem2 (table $table) (i32.const 0) func $trampoline_A $trampoline_B $trampoline_A) + + ;; PRIMARY: (export "table" (table $table)) + (export "table" (table $table)) + (elem $primary-elem1 (table $table) (i32.const 0) func $A $B $C) + (elem $primary-elem2 (table $table) (i32.const 0) func $A $B $A) + ;; MOD1: (elem $A-elem (table $table) (i32.const 0) func $A $A $A) + (elem $A-elem (table $table) (i32.const 0) func $A $A $A) + ;; MOD2: (elem $B-elem (table $table) (i32.const 0) func $B $B $B) + (elem $B-elem (table $table) (i32.const 0) func $B $B $B) + ;; MOD3: (elem $C-elem (table $table) (i32.const 0) func $C $C $C) + (elem $C-elem (table $table) (i32.const 0) func $C $C $C) + + ;; MOD1: (func $A + ;; MOD1-NEXT: (call_indirect (type $0) + ;; MOD1-NEXT: (i32.const 0) + ;; MOD1-NEXT: ) + ;; MOD1-NEXT: ) + (func $A + (call_indirect $table + (i32.const 0) + ) + ) + + ;; MOD2: (func $B + ;; MOD2-NEXT: ) + (func $B + ) + + ;; MOD3: (func $C + ;; MOD3-NEXT: ) + (func $C + ) +) diff --git a/test/lit/wasm-split/split-elems-no-placeholders.wast b/test/lit/wasm-split/split-elems-no-placeholders.wast new file mode 100644 index 00000000000..e62a7da224c --- /dev/null +++ b/test/lit/wasm-split/split-elems-no-placeholders.wast @@ -0,0 +1,94 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-split %s -all --no-placeholders -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep +;; RUN: wasm-dis %t.1.wasm -all | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm -all | filecheck %s --check-prefix SECONDARY + +;; When placeholders are NOT used and all functions in an element segment belong +;; to a single secondary module, we can move the segment to that secondary +;; module. + +(module + ;; PRIMARY: (global $keep-global funcref (ref.func $trampoline_split)) + (global $keep-global funcref (ref.func $split)) + ;; PRIMARY: (table $keep-table 2 2 funcref) + (table $keep-table 2 2 funcref) + ;; PRIMARY: (table $keep-table2 1 1 externref) + (table $keep-table2 1 1 externref) + ;; This contains both a primary function and a secondary function, so keep this + ;; in the primary. + ;; PRIMARY: (elem $keep-elem1 (table $keep-table) (i32.const 0) func $keep $trampoline_split) + (elem $keep-elem1 (table $keep-table) (i32.const 0) func $keep $split) + ;; This contains a global.get, so keep it in the primary. + ;; PRIMARY: (elem $keep-elem2 (table $keep-table) (i32.const 0) funcref (item (ref.func $trampoline_split)) (item (global.get $keep-global))) + (elem $keep-elem2 (table $keep-table) (i32.const 0) funcref (ref.func $split) (global.get $keep-global)) + ;; This is not a funcref table, and the referenced table is kept in the + ;; primary. So keep this in the primary too. + ;; PRIMARY: (elem $keep-elem3 (table $keep-table2) (i32.const 0) externref (item (ref.null noextern))) + (elem $keep-elem3 (table $keep-table2) (i32.const 0) externref (ref.null extern)) + + ;; SECONDARY: (global $split-global funcref (ref.func $split)) + (global $split-global funcref (ref.func $split)) + ;; SECONDARY: (table $split-table 2 2 funcref) + (table $split-table 2 2 funcref) + + ;; All functions are in the secondary module, so split this to the secondary, + ;; even if the referenced table is in the primary module. + ;; SECONDARY: (elem $split-elem1 (table $keep-table) (i32.const 0) func $split $split) + (elem $split-elem1 (table $keep-table) (i32.const 0) func $split $split) + ;; All functions are in the secondary module, so split this to the secondary. + ;; SECONDARY: (elem $split-elem2 (table $split-table) (i32.const 0) func $split $split) + (elem $split-elem2 (table $split-table) (i32.const 0) func $split $split) + ;; ref.null within data doesn't affect the segment's splitability. + ;; SECONDARY: (elem $split-elem3 (table $split-table) (i32.const 0) funcref (item (ref.func $split)) (item (ref.null nofunc))) + (elem $split-elem3 (table $split-table) (i32.const 0) funcref (ref.func $split) (ref.null nofunc)) + + ;; PRIMARY: (func $keep (type $0) + ;; PRIMARY-NEXT: (call_indirect $keep-table (type $0) + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: (drop + ;; PRIMARY-NEXT: (table.get $keep-table2 + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: (drop + ;; PRIMARY-NEXT: (global.get $keep-global) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $keep + ;; Uses $keep-table + (call_indirect $keep-table + (i32.const 0) + ) + ;; Uses $keep-table2 + (drop + (table.get $keep-table2 + (i32.const 0) + ) + ) + ;; Uses $keep-global + (drop + (global.get $keep-global) + ) + ) + + ;; SECONDARY: (func $split (type $0) + ;; SECONDARY-NEXT: (call_indirect $split-table (type $0) + ;; SECONDARY-NEXT: (i32.const 0) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: (drop + ;; SECONDARY-NEXT: (global.get $split-global) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: ) + (func $split + ;; Uses $split-table + (call_indirect $split-table + (i32.const 0) + ) + ;; Uses $split-global + (drop + (global.get $split-global) + ) + ) +) + From e04fb9afbcbd40f10ce0f26564b3824abe52e5eb Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 18 Jun 2026 05:23:57 +0000 Subject: [PATCH 2/5] Add an out-of-bounds test --- test/lit/wasm-split/split-elems-no-placeholders.wast | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/lit/wasm-split/split-elems-no-placeholders.wast b/test/lit/wasm-split/split-elems-no-placeholders.wast index e62a7da224c..a9002b48472 100644 --- a/test/lit/wasm-split/split-elems-no-placeholders.wast +++ b/test/lit/wasm-split/split-elems-no-placeholders.wast @@ -25,6 +25,9 @@ ;; primary. So keep this in the primary too. ;; PRIMARY: (elem $keep-elem3 (table $keep-table2) (i32.const 0) externref (item (ref.null noextern))) (elem $keep-elem3 (table $keep-table2) (i32.const 0) externref (ref.null extern)) + ;; The offset exceeds the table size, so keep this in the primary. + ;; PRIMARY: (elem $keep-elem4 (table $keep-table) (i32.const 10) func $trampoline_split) + (elem $keep-elem4 (table $keep-table) (i32.const 10) func $split) ;; SECONDARY: (global $split-global funcref (ref.func $split)) (global $split-global funcref (ref.func $split)) @@ -35,7 +38,7 @@ ;; even if the referenced table is in the primary module. ;; SECONDARY: (elem $split-elem1 (table $keep-table) (i32.const 0) func $split $split) (elem $split-elem1 (table $keep-table) (i32.const 0) func $split $split) - ;; All functions are in the secondary module, so split this to the secondary. + ;; The same test with $split-table. ;; SECONDARY: (elem $split-elem2 (table $split-table) (i32.const 0) func $split $split) (elem $split-elem2 (table $split-table) (i32.const 0) func $split $split) ;; ref.null within data doesn't affect the segment's splitability. From 16d4d43e281d43329741bd32a024ebe72254ede1 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 18 Jun 2026 21:52:22 +0000 Subject: [PATCH 3/5] Add an array.init_elem test --- .../split-elems-no-placeholders.wast | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/lit/wasm-split/split-elems-no-placeholders.wast b/test/lit/wasm-split/split-elems-no-placeholders.wast index a9002b48472..a284c1a7a77 100644 --- a/test/lit/wasm-split/split-elems-no-placeholders.wast +++ b/test/lit/wasm-split/split-elems-no-placeholders.wast @@ -8,12 +8,16 @@ ;; module. (module + ;; PRIMARY: (type $array (array (mut funcref))) + (type $array (array (mut funcref))) + ;; PRIMARY: (global $keep-global funcref (ref.func $trampoline_split)) (global $keep-global funcref (ref.func $split)) ;; PRIMARY: (table $keep-table 2 2 funcref) (table $keep-table 2 2 funcref) ;; PRIMARY: (table $keep-table2 1 1 externref) (table $keep-table2 1 1 externref) + ;; This contains both a primary function and a secondary function, so keep this ;; in the primary. ;; PRIMARY: (elem $keep-elem1 (table $keep-table) (i32.const 0) func $keep $trampoline_split) @@ -28,6 +32,11 @@ ;; The offset exceeds the table size, so keep this in the primary. ;; PRIMARY: (elem $keep-elem4 (table $keep-table) (i32.const 10) func $trampoline_split) (elem $keep-elem4 (table $keep-table) (i32.const 10) func $split) + ;; This segment contains only secondary functions, but it is referenced by an + ;; array.init_elem instruction in the primary module, so keep this in the + ;; primary. + ;; PRIMARY: (elem $keep-elem5 (table $keep-table) (i32.const 0) func $trampoline_split) + (elem $keep-elem5 (table $keep-table) (i32.const 0) func $split) ;; SECONDARY: (global $split-global funcref (ref.func $split)) (global $split-global funcref (ref.func $split)) @@ -57,6 +66,14 @@ ;; PRIMARY-NEXT: (drop ;; PRIMARY-NEXT: (global.get $keep-global) ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: (array.init_elem $array $keep-elem5 + ;; PRIMARY-NEXT: (array.new_default $array + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) (func $keep ;; Uses $keep-table @@ -73,6 +90,13 @@ (drop (global.get $keep-global) ) + ;; References $keep-elem5 + (array.init_elem $array $keep-elem5 + (array.new_default $array (i32.const 1)) + (i32.const 0) + (i32.const 0) + (i32.const 1) + ) ) ;; SECONDARY: (func $split (type $0) From fdc5a39be6e6f78c2581d4edce3262691f5f23e4 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 24 Jun 2026 01:09:00 +0000 Subject: [PATCH 4/5] More comments --- src/ir/module-splitting.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index d3af7fa937c..fd01faef8e5 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -900,18 +900,25 @@ ModuleSplitter::PrimarySecondaryUsedNames ModuleSplitter::computeUsedNames() { bool keepInPrimary = false; for (auto* item : segment->data) { if (item->is()) { + // If we encounter a global.get, we just keep the segment in the + // primary module. TODO Consider moving this if this global is in a + // secondary module. keepInPrimary = true; break; } else if (auto* ref = item->dynCast()) { + // If this ref.func is in the primary module, keep the segment there. auto it = funcToSecondaryIndex.find(ref->func); if (it == funcToSecondaryIndex.end()) { keepInPrimary = true; break; } + // If the segment contains ref.funcs from more than one secondary + // modules, keep it in the primary. if (foundSecondary && secondaryIndex != it->second) { keepInPrimary = true; break; } + // This segment contains ref.funcs from a single secondary module. foundSecondary = true; secondaryIndex = it->second; } From 41f12c907ec8555e7e094aab44a9c357dd657c3b Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 24 Jun 2026 01:11:47 +0000 Subject: [PATCH 5/5] tweak test --- .../split-elems-no-placeholders.wast | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/lit/wasm-split/split-elems-no-placeholders.wast b/test/lit/wasm-split/split-elems-no-placeholders.wast index a284c1a7a77..69e713dbfed 100644 --- a/test/lit/wasm-split/split-elems-no-placeholders.wast +++ b/test/lit/wasm-split/split-elems-no-placeholders.wast @@ -66,14 +66,6 @@ ;; PRIMARY-NEXT: (drop ;; PRIMARY-NEXT: (global.get $keep-global) ;; PRIMARY-NEXT: ) - ;; PRIMARY-NEXT: (array.init_elem $array $keep-elem5 - ;; PRIMARY-NEXT: (array.new_default $array - ;; PRIMARY-NEXT: (i32.const 1) - ;; PRIMARY-NEXT: ) - ;; PRIMARY-NEXT: (i32.const 0) - ;; PRIMARY-NEXT: (i32.const 0) - ;; PRIMARY-NEXT: (i32.const 1) - ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) (func $keep ;; Uses $keep-table @@ -90,6 +82,21 @@ (drop (global.get $keep-global) ) + ) + + ;; This is not in --keep-funcs but will be kept in the primary because it + ;; references a segment. + ;; PRIMARY: (func $array-init-elem-user (type $0) + ;; PRIMARY-NEXT: (array.init_elem $array $keep-elem5 + ;; PRIMARY-NEXT: (array.new_default $array + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: (i32.const 0) + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $array-init-elem-user ;; References $keep-elem5 (array.init_elem $array $keep-elem5 (array.new_default $array (i32.const 1))