Skip to content

Add a --constant-arrays-to-data pass - #9274

Open
gkdn wants to merge 4 commits into
WebAssembly:mainfrom
gkdn:constant-arrays-to-data
Open

gkdn wants to merge 4 commits into
WebAssembly:mainfrom
gkdn:constant-arrays-to-data

Conversation

@gkdn

@gkdn gkdn commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #9248: a late pass that converts array.new_fixed of constant
numeric values in function bodies to array.new_data when that is smaller
(array.new_fixed needs a full *.const per element, a data segment stores raw
bytes).

Since array.new_fixed is a constant expression that other optimizations
understand, the default pipeline runs this only in the last set of opts
(lastOpts, #9238).

  • Global initializers are left alone, as array.new_data is not a constant
    expression there.
  • Converted payloads go into one new passive segment. Identical payloads are
    stored once, and a payload identical to an existing, never-dropped passive
    segment reuses that segment.
  • A group of identical payloads is converted only if the estimated binary size
    is smaller, and the new segment is only added if the savings that depend on it
    pay for its header.
  • The segment is never dropped and all reads are in bounds, so the result
    cannot trap.

Note that this does not make it safe to raise the 64-element cap in
OptimizeInstructions: the conversion is not always reversible. A larger
array.new_fixed can be hoisted into a global initializer, where it cannot be
turned back into array.new_data. On a large J2Wasm binary, removing the cap
moved the remaining big tables into globals and grew the binary (+0.9 KB raw,
+1.4 KB gzipped), which this pass cannot recover.

gkdn added 4 commits October 16, 2026 17:16
Convert array.new_fixed of constant numeric values in function bodies to
array.new_data when that is smaller in the binary. array.new_fixed needs a
full *.const per element, while a data segment stores raw bytes.

As array.new_fixed is a constant expression that other optimizations
understand, this is only done in the last set of default optimizations
(lastOpts).

All converted payloads go into a single new passive segment. Identical
payloads are stored once, and payloads that already appear in the new
segment or in an existing passive segment that is never dropped reuse
those bytes.
* Copy debug locations to the new array.new_data.
* Use FindAllPointers and FindAll in the parallel analysis (which now
  also collects the dropped segments), Builder::makeDataSegment, and the
  LEB writers to measure sizes.
* Only add the new segment if the conversions that use it pay for its
  header, and still reuse existing segments otherwise.
* Reuse existing segments only if they are identical to the payload, to
  avoid a quadratic substring search (left as a TODO, along with
  array.new_elem).
* Check all module items in the lit tests (--all-items), including the
  new segment's contents and debug locations.
* Move the size estimates into helpers: getArrayNewFixedSize,
  getArrayNewDataSize and getNewSegmentOverhead.
* Split run() into findCandidates, groupByPayload, decide and replace.
* Build payloads as std::vector<char> (like DataSegment::data) with
  getBits and memcpy, as the interpreter does, instead of casting.

No functional change.
* Order the pass top-down: run, then each step in the order it is
  called, with the helpers of planConversions after it.
* Rename Group to PayloadGroup, Decision to Conversion, decide to
  planConversions, replace to applyConversions, and addSegment to
  addNewSegment. Store the array in Candidate instead of a get() method.
* Plan without modifying the module: the new segment's contents are
  gathered in newSegmentData, and run() adds the segment.
* Comment the non-obvious parts: why unreachable, empty and non-numeric
  arrays are skipped, why dropped segments cannot be reused, why the
  result cannot trap, pointer stability of the candidates, and why the
  new segment's cost is checked after all groups.

No functional change.
@gkdn
gkdn requested a review from a team as a code owner October 17, 2026 04:35
@gkdn
gkdn requested review from tlively and removed request for a team October 17, 2026 04:35

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the --constant-arrays-to-data optimization pass, which converts array.new_fixed instructions of constant numeric values into array.new_data from passive data segments when it reduces binary size. The pass is integrated into the end of the default optimization pipeline. The review feedback suggests avoiding registering the pass if the bulk-memory feature is disabled, and recommends replacing functional-style casts with explicit static_cast calls to avoid potential narrowing warnings on 64-bit platforms and conform to modern C++ practices.

Comment thread src/passes/pass.cpp
Comment on lines +860 to +861
if (options.lastOpts && wasm->features.hasGC() &&
(options.optimizeLevel >= 2 || options.shrinkLevel >= 1)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the constant-arrays-to-data pass requires the bulk-memory feature (as checked in ConstantArraysToData::run), we can avoid adding the pass to the runner entirely if bulk-memory is not enabled. This saves a small amount of overhead.

Suggested change
if (options.lastOpts && wasm->features.hasGC() &&
(options.optimizeLevel >= 2 || options.shrinkLevel >= 1)) {
if (options.lastOpts && wasm->features.hasGC() && wasm->features.hasBulkMemory() &&
(options.optimizeLevel >= 2 || options.shrinkLevel >= 1)) {

// The size of an array.new_fixed of constants, apart from the prefix, opcode
// and type index, which are the same in array.new_data.
size_t getArrayNewFixedSize(ArrayNewFixed* curr) {
size_t size = getWrittenSize(U32LEB(curr->values.size()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

On 64-bit platforms, curr->values.size() is of type size_t (64-bit), whereas U32LEB expects a uint32_t (32-bit). This can trigger narrowing or conversion warnings on some compilers (especially with strict warning settings or -Wconversion). It is safer to explicitly cast it using static_cast<uint32_t> per the Google C++ Style Guide.

Suggested change
size_t size = getWrittenSize(U32LEB(curr->values.size()));
size_t size = getWrittenSize(U32LEB(static_cast<uint32_t>(curr->values.size())));

// The size of a new passive data segment, apart from its data.
size_t getNewSegmentOverhead(const Module& module, size_t dataSize) {
// The flags and the size of the data.
size_t size = 1 + getWrittenSize(U32LEB(dataSize));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

On 64-bit platforms, dataSize is of type size_t (64-bit), whereas U32LEB expects a uint32_t (32-bit). This can trigger narrowing or conversion warnings on some compilers. It is safer to explicitly cast it using static_cast<uint32_t> per the Google C++ Style Guide.

Suggested change
size_t size = 1 + getWrittenSize(U32LEB(dataSize));
size_t size = 1 + getWrittenSize(U32LEB(static_cast<uint32_t>(dataSize)));

Comment on lines +107 to +109
return getWrittenSize(U32LEB(segment)) + 1 +
getWrittenSize(S32LEB(int32_t(offset))) + 1 +
getWrittenSize(S32LEB(int32_t(size)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using C-style or functional-style casts like int32_t(offset) is discouraged in modern C++. It is safer and more idiomatic to use static_cast<int32_t> per the Google C++ Style Guide.

Suggested change
return getWrittenSize(U32LEB(segment)) + 1 +
getWrittenSize(S32LEB(int32_t(offset))) + 1 +
getWrittenSize(S32LEB(int32_t(size)));
return getWrittenSize(U32LEB(segment)) + 1 +
getWrittenSize(S32LEB(static_cast<int32_t>(offset))) + 1 +
getWrittenSize(S32LEB(static_cast<int32_t>(size)));

Comment on lines +282 to +283
auto savings = getCodeSavings(group, newSegmentIndex, offset) -
int64_t(group.payload.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using C-style or functional-style casts like int64_t(group.payload.size()) is discouraged in modern C++. It is safer and more idiomatic to use static_cast<int64_t> per the Google C++ Style Guide.

Suggested change
auto savings = getCodeSavings(group, newSegmentIndex, offset) -
int64_t(group.payload.size());
auto savings = getCodeSavings(group, newSegmentIndex, offset) -
static_cast<int64_t>(group.payload.size());

Comment on lines +296 to +297
newSegmentSavings <=
int64_t(getNewSegmentOverhead(*module, newSegmentData.size()))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using C-style or functional-style casts like int64_t(...) is discouraged in modern C++. It is safer and more idiomatic to use static_cast<int64_t> per the Google C++ Style Guide.

Suggested change
newSegmentSavings <=
int64_t(getNewSegmentOverhead(*module, newSegmentData.size()))) {
newSegmentSavings <=
static_cast<int64_t>(getNewSegmentOverhead(*module, newSegmentData.size()))) {

Comment on lines +328 to +330
savings +=
int64_t(getArrayNewFixedSize(array)) -
int64_t(getArrayNewDataSize(segment, offset, array->values.size()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using C-style or functional-style casts like int64_t(...) is discouraged in modern C++. It is safer and more idiomatic to use static_cast<int64_t> per the Google C++ Style Guide.

Suggested change
savings +=
int64_t(getArrayNewFixedSize(array)) -
int64_t(getArrayNewDataSize(segment, offset, array->values.size()));
savings +=
static_cast<int64_t>(getArrayNewFixedSize(array)) -
static_cast<int64_t>(getArrayNewDataSize(segment, offset, array->values.size()));

Comment on lines +353 to +354
builder.makeConst(int32_t(conversion.offset)),
builder.makeConst(int32_t(array->values.size())));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using C-style or functional-style casts like int32_t(...) is discouraged in modern C++. It is safer and more idiomatic to use static_cast<int32_t> per the Google C++ Style Guide.

Suggested change
builder.makeConst(int32_t(conversion.offset)),
builder.makeConst(int32_t(array->values.size())));
builder.makeConst(static_cast<int32_t>(conversion.offset)),
builder.makeConst(static_cast<int32_t>(array->values.size())));

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant