Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Current Trunk

- Add a new wasm-embed tool for extracting Wasm modules embedded as byte arrays
in JS source files or replacing such modules in the JS source (#9127)
- Add a new `--constant-pooling` pass, which pools repeated numeric constants
into immutable globals. (#9163)

v133
----
Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ set(passes_SOURCES
CodeFolding.cpp
ConstantFieldPropagation.cpp
ConstHoisting.cpp
ConstantPooling.cpp
ConstraintAnalysis.cpp
DataFlowOpts.cpp
DeadArgumentElimination.cpp
Expand Down
220 changes: 220 additions & 0 deletions src/passes/ConstantPooling.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* Copyright 2026 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//
// Pools repeated constants into immutable globals. A constant that is used
// multiple times can often be stored in a global, replacing the bytes of
// encoding the constant each time with the (usually smaller) bytes of a
// global.get. We only do this when the estimated binary size strictly
// decreases, so small constants (which are already very cheap) are left alone.
//
// WARNING: like const-hoisting, this shrinks raw size but can increase gzip
// size, as removing repeated constants removes redundancy that
// compressors use.
//

#include <cstring>
#include <map>
#include <string>

#include "ir/module-utils.h"
#include "pass.h"
#include "wasm-binary.h"
#include "wasm-builder.h"
#include "wasm.h"

namespace wasm {

namespace {

// The types of constants we consider pooling. More types are possible, but
// these are the ones where pooling can typically pay off.
bool isPoolable(Type type) {
return type == Type::i32 || type == Type::i64 || type == Type::f32 ||
type == Type::f64 || type == Type::v128;
}

// The number of bytes an unsigned LEB128 takes.
Index sizeULEB(uint64_t value) {
Index size = 1;
while (value >= 0x80) {
value >>= 7;
size++;
}
return size;
}

template<typename T> Index getWrittenSize(const T& thing) {
BufferWithRandomAccess buffer;
buffer << thing;
return buffer.size();
}

// The number of bytes a constant instruction takes in the binary, including
// the opcode.
Index getConstSize(const Literal& value) {
auto type = value.type;
if (type == Type::i32) {
return 1 + getWrittenSize(S32LEB(value.geti32()));
}
if (type == Type::i64) {
return 1 + getWrittenSize(S64LEB(value.geti64()));
}
if (type == Type::f32) {
return 1 + 4;
}
if (type == Type::f64) {
return 1 + 8;
}
assert(type == Type::v128);
// v128.const has a two-byte prefixed opcode (0xfd 0x0c), unlike the other
// constant instructions, followed by the 16 bytes of the value.
return 2 + 16;
}

// A deterministic key for a constant: the basic type and the raw bits. We
// cannot use Literal directly as its comparison depends on Type IDs, which are
// not stable between runs.
struct PoolKey {
Type::BasicType type;
uint8_t bits[16];

bool operator<(const PoolKey& other) const {
if (type != other.type) {
return type < other.type;
}
return memcmp(bits, other.bits, 16) < 0;
}
};

struct ConstantPooling : public Pass {
// We add globals, but do not change any of the locals in the module.
bool requiresNonNullableLocalFixups() override { return false; }

void run(Module* module) override {
using Ptrs = std::vector<Expression**>;

// Scan all the function bodies. We cannot handle constants in const-
// expression positions (global initializers, segment offsets, etc.), as
// those cannot contain a global.get (of a non-imported global), so we only
// walk function bodies.
struct Scanner : public PostWalker<Scanner> {
Ptrs& ptrs;

Scanner(Ptrs& ptrs) : ptrs(ptrs) {}

void visitConst(Const* curr) {
if (isPoolable(curr->type)) {
ptrs.push_back(getCurrentPointer());
}
}
};

ModuleUtils::ParallelFunctionAnalysis<Ptrs> analysis(
*module, [&](Function* func, Ptrs& ptrs) {
if (!func->imported()) {
Scanner(ptrs).walk(func->body);
}
});

// Group the constants by their value.
std::map<PoolKey, Ptrs> groups;
for (auto& [func, ptrs] : analysis.map) {
for (auto** ptr : ptrs) {
auto* curr = (*ptr)->cast<Const>();
PoolKey key;
key.type = curr->type.getBasic();
curr->value.getBits(key.bits);
groups[key].push_back(ptr);
}
}

// Decide which constants to pool. A pooled constant adds one global
// (whose entry encodes the constant plus overhead) and replaces each use
// with a global.get.
Index existingGlobals = module->globals.size();
auto globalGetSize = [&](Index index) { return 1 + sizeULEB(index); };

struct ToPool {
Const* value;
Ptrs* ptrs;
Name name;
};
std::vector<ToPool> toPool;

// The index a pooled global will get. We do not know that for sure, but we
// compute a conservative bound: if there will be more than 128 globals
// then some may need two bytes to encode, so assume that.
Index numGlobals = existingGlobals + groups.size();
Index getSize = globalGetSize(numGlobals > 0 ? numGlobals - 1 : 0);

// If the module has no globals at all, we must also pay for the global
// section itself when we add the first one.
bool firstPooled = true;

for (auto& [key, ptrs] : groups) {
auto* value = (*ptrs[0])->cast<Const>();
Index constSize = getConstSize(value->value);
Index count = ptrs.size();
// This is only valid if the global.get is smaller than the constant.
if (constSize <= getSize) {
continue;
}
int64_t savings =
int64_t(count) * (constSize - getSize) - (constSize + 3);
if (existingGlobals == 0 && firstPooled) {
savings -= 3;
}
if (savings > 0) {
toPool.push_back({value, &ptrs, Name()});
firstPooled = false;
}
}

if (toPool.empty()) {
return;
}

// Create the globals, then replace the uses.
Builder builder(*module);
Index counter = 0;
for (auto& entry : toPool) {
Name name;
while (true) {
name = std::string("const$") + std::to_string(counter++);
if (!module->getGlobalOrNull(name)) {
break;
}
}
entry.name = name;
auto global = builder.makeGlobal(
name, entry.value->type, entry.value, Builder::Immutable);
module->addGlobal(std::move(global));
}

for (auto& entry : toPool) {
for (auto** ptr : *entry.ptrs) {
*ptr = builder.makeGlobalGet(entry.name, entry.value->type);
}
}
}
};

} // anonymous namespace

Pass* createConstantPoolingPass() { return new ConstantPooling(); }

} // namespace wasm
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ void PassRegistry::registerPasses() {
registerPass("const-hoisting",
"hoist repeated constants to a local",
createConstHoistingPass);
registerPass("constant-pooling",
"pool repeated constants in globals",
createConstantPoolingPass);
registerPass("cfp",
"propagate constant struct field values",
createConstantFieldPropagationPass);
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Pass* createCoalesceLocalsWithLearningPass();
Pass* createCodeFoldingPass();
Pass* createCodePushingPass();
Pass* createConstHoistingPass();
Pass* createConstantPoolingPass();
Pass* createConstantFieldPropagationPass();
Pass* createConstantFieldPropagationRefTestPass();
Pass* createConstraintAnalysisPass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-EMPTY:
;; CHECK-NEXT: --constant-pooling pool repeated constants in
;; CHECK-NEXT: globals
;; CHECK-EMPTY:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-EMPTY:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-EMPTY:
;; CHECK-NEXT: --constant-pooling pool repeated constants in
;; CHECK-NEXT: globals
;; CHECK-EMPTY:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-EMPTY:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
;; CHECK-NEXT: --const-hoisting hoist repeated constants to a
;; CHECK-NEXT: local
;; CHECK-EMPTY:
;; CHECK-NEXT: --constant-pooling pool repeated constants in
;; CHECK-NEXT: globals
;; CHECK-EMPTY:
;; CHECK-NEXT: --constraint-analysis finds and uses mathematical
;; CHECK-NEXT: constraints on locals
;; CHECK-EMPTY:
Expand Down
80 changes: 80 additions & 0 deletions test/lit/passes/constant-pooling.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
;; RUN: foreach %s %t wasm-opt --all-features --constant-pooling -S -o - | filecheck %s

;; Repeated constants that are expensive to encode are pooled in globals. Cheap
;; constants (like i32.const 0) are left alone, and constants in const
;; expression positions (global initializers) are never replaced. Also, a
;; large i32 constant needs more uses than a float to pay for its global.

(module
;; CHECK: (type $0 (func (result i32)))

;; CHECK: (type $1 (func (result f64)))

;; CHECK: (type $2 (func (result v128)))

;; CHECK: (global $existing f64 (f64.const 1.5))
(global $existing f64
(f64.const 1.5)
)
;; CHECK: (global $const$0 i32 (i32.const 305419896))

;; CHECK: (global $const$1 f64 (f64.const 1.5))

;; CHECK: (global $const$2 v128 (v128.const i32x4 0x00000000 0x00000001 0x00000002 0x00000003))

;; CHECK: (func $double (type $1) (result f64)
;; CHECK-NEXT: (global.get $const$1)
;; CHECK-NEXT: )
(func $double (result f64)
(f64.const 1.5)
)
;; CHECK: (func $also-double (type $1) (result f64)
;; CHECK-NEXT: (global.get $const$1)
;; CHECK-NEXT: )
(func $also-double (result f64)
(f64.const 1.5)
)
;; CHECK: (func $zero (type $0) (result i32)
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
(func $zero (result i32)
(i32.const 0)
)
;; CHECK: (func $also-zero (type $0) (result i32)
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
(func $also-zero (result i32)
(i32.const 0)
)
;; CHECK: (func $vector (type $2) (result v128)
;; CHECK-NEXT: (global.get $const$2)
;; CHECK-NEXT: )
(func $vector (result v128)
(v128.const i32x4 0 1 2 3)
)
;; CHECK: (func $also-vector (type $2) (result v128)
;; CHECK-NEXT: (global.get $const$2)
;; CHECK-NEXT: )
(func $also-vector (result v128)
(v128.const i32x4 0 1 2 3)
)
;; CHECK: (func $fat-const (type $0) (result i32)
;; CHECK-NEXT: (global.get $const$0)
;; CHECK-NEXT: )
(func $fat-const (result i32)
(i32.const 0x12345678)
)
;; CHECK: (func $also-fat-const (type $0) (result i32)
;; CHECK-NEXT: (global.get $const$0)
;; CHECK-NEXT: )
(func $also-fat-const (result i32)
(i32.const 0x12345678)
)
;; CHECK: (func $third-fat-const (type $0) (result i32)
;; CHECK-NEXT: (global.get $const$0)
;; CHECK-NEXT: )
(func $third-fat-const (result i32)
(i32.const 0x12345678)
)
)