From cfd5b0cd2943efb72728f619f31e3bb268b86475 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 17 Jun 2026 07:41:49 +0000 Subject: [PATCH 1/2] [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/2] 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.