diff --git a/CHANGELOG.md b/CHANGELOG.md index b6cab75178f..2e6f310dcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ---- diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index ab08d3a410b..02f7eef7a93 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -26,6 +26,7 @@ set(passes_SOURCES CodeFolding.cpp ConstantFieldPropagation.cpp ConstHoisting.cpp + ConstantPooling.cpp ConstraintAnalysis.cpp DataFlowOpts.cpp DeadArgumentElimination.cpp diff --git a/src/passes/ConstantPooling.cpp b/src/passes/ConstantPooling.cpp new file mode 100644 index 00000000000..7e15e5b7c54 --- /dev/null +++ b/src/passes/ConstantPooling.cpp @@ -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 +#include +#include + +#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 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; + + // 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 { + Ptrs& ptrs; + + Scanner(Ptrs& ptrs) : ptrs(ptrs) {} + + void visitConst(Const* curr) { + if (isPoolable(curr->type)) { + ptrs.push_back(getCurrentPointer()); + } + } + }; + + ModuleUtils::ParallelFunctionAnalysis analysis( + *module, [&](Function* func, Ptrs& ptrs) { + if (!func->imported()) { + Scanner(ptrs).walk(func->body); + } + }); + + // Group the constants by their value. + std::map groups; + for (auto& [func, ptrs] : analysis.map) { + for (auto** ptr : ptrs) { + auto* curr = (*ptr)->cast(); + 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; + + // 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(); + 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 diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index db7e769c505..0ce83128c0d 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -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); diff --git a/src/passes/passes.h b/src/passes/passes.h index 8a27158ecb0..bace3dd2e4c 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -31,6 +31,7 @@ Pass* createCoalesceLocalsWithLearningPass(); Pass* createCodeFoldingPass(); Pass* createCodePushingPass(); Pass* createConstHoistingPass(); +Pass* createConstantPoolingPass(); Pass* createConstantFieldPropagationPass(); Pass* createConstantFieldPropagationRefTestPass(); Pass* createConstraintAnalysisPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 0be805fa01f..ad58255f8e2 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -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: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 116c3260dcb..69bb58319d1 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -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: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 14388cd93cb..8928a187198 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -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: diff --git a/test/lit/passes/constant-pooling.wast b/test/lit/passes/constant-pooling.wast new file mode 100644 index 00000000000..dd4423529b9 --- /dev/null +++ b/test/lit/passes/constant-pooling.wast @@ -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) + ) +)