Repository navigation
Conversation
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.
There was a problem hiding this comment.
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.
| if (options.lastOpts && wasm->features.hasGC() && | ||
| (options.optimizeLevel >= 2 || options.shrinkLevel >= 1)) { |
There was a problem hiding this comment.
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.
| 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())); |
There was a problem hiding this comment.
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.
| 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)); |
There was a problem hiding this comment.
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.
| size_t size = 1 + getWrittenSize(U32LEB(dataSize)); | |
| size_t size = 1 + getWrittenSize(U32LEB(static_cast<uint32_t>(dataSize))); |
| return getWrittenSize(U32LEB(segment)) + 1 + | ||
| getWrittenSize(S32LEB(int32_t(offset))) + 1 + | ||
| getWrittenSize(S32LEB(int32_t(size))); |
There was a problem hiding this comment.
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.
| 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))); |
| auto savings = getCodeSavings(group, newSegmentIndex, offset) - | ||
| int64_t(group.payload.size()); |
There was a problem hiding this comment.
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.
| auto savings = getCodeSavings(group, newSegmentIndex, offset) - | |
| int64_t(group.payload.size()); | |
| auto savings = getCodeSavings(group, newSegmentIndex, offset) - | |
| static_cast<int64_t>(group.payload.size()); |
| newSegmentSavings <= | ||
| int64_t(getNewSegmentOverhead(*module, newSegmentData.size()))) { |
There was a problem hiding this comment.
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.
| newSegmentSavings <= | |
| int64_t(getNewSegmentOverhead(*module, newSegmentData.size()))) { | |
| newSegmentSavings <= | |
| static_cast<int64_t>(getNewSegmentOverhead(*module, newSegmentData.size()))) { |
| savings += | ||
| int64_t(getArrayNewFixedSize(array)) - | ||
| int64_t(getArrayNewDataSize(segment, offset, array->values.size())); |
There was a problem hiding this comment.
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.
| 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())); |
| builder.makeConst(int32_t(conversion.offset)), | ||
| builder.makeConst(int32_t(array->values.size()))); |
There was a problem hiding this comment.
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.
| 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()))); |
Follow-up to #9248: a late pass that converts
array.new_fixedof constantnumeric values in function bodies to
array.new_datawhen that is smaller(
array.new_fixedneeds a full*.constper element, a data segment stores rawbytes).
Since
array.new_fixedis a constant expression that other optimizationsunderstand, the default pipeline runs this only in the last set of opts
(
lastOpts, #9238).array.new_datais not a constantexpression there.
stored once, and a payload identical to an existing, never-dropped passive
segment reuses that segment.
is smaller, and the new segment is only added if the savings that depend on it
pay for its header.
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_fixedcan be hoisted into a global initializer, where it cannot beturned back into
array.new_data. On a large J2Wasm binary, removing the capmoved the remaining big tables into globals and grew the binary (+0.9 KB raw,
+1.4 KB gzipped), which this pass cannot recover.