Skip to content
Merged
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
7 changes: 6 additions & 1 deletion src/literal.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <array>
#include <iostream>
#include <optional>
#include <variant>

#include "support/bits.h"
Expand Down Expand Up @@ -213,7 +214,11 @@ class Literal {
}
}

static Literal makeFromMemory(const void* p, Type type);
// Reads a value of the given type from memory. For i32, a smaller byteSize
// (1 or 2) can be given to read a sign-extended packed value. By default the
// full size of the type is read.
static Literal makeFromMemory(
const void* p, Type type, std::optional<unsigned> byteSize = std::nullopt);

static Literal makeSignedMin(Type type) {
switch (type.getBasic()) {
Expand Down
88 changes: 88 additions & 0 deletions src/passes/OptimizeInstructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2349,6 +2349,94 @@ struct OptimizeInstructions
replaceCurrent(block);
}

void visitArrayNewData(ArrayNewData* curr) {
// Lower array.new_data with constant operands to array.new_default,
// array.new or array.new_fixed. Unlike array.new_data, those are constant
// expressions, and more optimizations understand them.
if (curr->type == Type::unreachable) {
return;
}

auto* offsetConst = curr->offset->dynCast<Const>();
auto* sizeConst = curr->size->dynCast<Const>();
if (!offsetConst || !sizeConst) {
return;
}

auto* seg = getModule()->getDataSegment(curr->segment);
if (!seg->isPassive()) {
Comment thread
kripken marked this conversation as resolved.
// Active segments are implicitly dropped upon module instantiation.
return;
}

auto heapType = curr->type.getHeapType();
const auto& element = heapType.getArray().element;

Address offset = offsetConst->value.getUnsigned();
Address size = sizeConst->value.getUnsigned();
Address elemBytes = element.getByteSize();
Address end;
if (std::ckd_add<uint64_t>(&end.addr, offset, size * elemBytes) ||
end > seg->data.size()) {
// This will trap.
return;
}

// A dropped segment acts as if it had length 0, so unless end == 0 this
// traps if the segment was dropped at runtime, which we can't rule out.
// TODO: Optimize without TNH if the segment is never dropped.
if (end > 0 && !getPassOptions().trapsNeverHappen) {
return;
}

Builder builder(*getModule());

if (size == 0) {
replaceCurrent(builder.makeArrayNewFixed(heapType, {}));
return;
}

auto readElement = [&](Index i) -> Literal {
const auto* src = &seg->data[offset + i * elemBytes];
// Packed values are truncated when stored, so the extension doesn't
// matter; sign-extending gives the shorter encoding (-1 vs 255).
return Literal::makeFromMemory(src, element.type, element.getByteSize());
};

// If all elements are equal, array.new is compact at any size.
auto first = readElement(0);
bool allEqual = true;
for (Index i = 1; i < size; i++) {
if (readElement(i) != first) {
allEqual = false;
break;
}
}
if (allEqual) {
// visitArrayNew optimizes this further when the value is the default
// (to array.new_default) or the size is 1 (to array.new_fixed).
auto* sizeExpr = builder.makeConst(int32_t(size));
replaceCurrent(
builder.makeArrayNew(heapType, sizeExpr, builder.makeConst(first)));
return;
}

// array.new_fixed needs a full *.const per element while the segment
// stores raw bytes, so only do this for small arrays. Empirically, 64 sits
// in a natural gap between small/medium array literals (2D array rows,
// enum/switch tables, etc.) and large bulk lookup tables (100+ elements).
constexpr uint64_t MaxArrayNewFixedSize = 64;

@kripken kripken Oct 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How about putting this behind options.shrinkLevel == 0 (i.e. not doing it in -Os/-Oz)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Though, even so, 64 elements is a large increase in size. How likely is it that it gets optimized later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For CalcWorker, this was actually the good cut off for the smallest size. The main goal here is to make these constant expressions so they can be used for global initialization which later enables further optimizations (in our case esp. clinit constant hoisting taking advantage of that).

But that might not be true for all apps. If you like, we can put it behind options.shrinkLevel == 0 if you prefer or make the number itself configurable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, I am worried about the generalization to other codebases. But I don't have a clear intuition here...

Looks like Kotlin and Dart also use ArrayNewData. On some large binaries I saw no difference on Dart (no new optimization was possible), while on Kotlin this regressed size, with this diff:

  [globals]      : 13090   
  [imports]      : 568     
  [memories]     : 1       
- [memory-data]  : 1326742 
+ [memory-data]  : 1325066 
  [tables]       : 0       
  [tags]         : 1       
- [total]        : 1337835 
+ [total]        : 1338412 
  [vars]         : 33597   
  ArrayCopy      : 17      
  ArrayGet       : 2494    
  ArrayLen       : 950     
  ArrayNew       : 97      
- ArrayNewData   : 48      
- ArrayNewFixed  : 435     
+ ArrayNewData   : 23      
+ ArrayNewFixed  : 460     
  ArraySet       : 917     
  Binary         : 34293   
  Block          : 27894   
  Break          : 8450    
  Call           : 80567   
  CallRef        : 7548    
- Const          : 143375  
+ Const          : 143952  
  Drop           : 6746    
  GlobalGet      : 32620   
  GlobalSet      : 2941    

Looks like half the ArrayNewDatas were optimized out, into new and larger ArrayNewFixed which could not be optimized out.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall this added 786 bytes, but that is not significant in a 5 MB binary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should have a high threshold here, then add a late-running pass that goes the other way and replaces large array.new_fixed with array.new_data/elem as a size optimization. Accepting the small size regression for some producers in the meantime seems fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just note that if produces are not aware of that late pass, they will get a hit. Anyway I already starting experimenting with the late pass tonight; I need to work on what a good number here is.

Interesting that the j2cl pass is needed. I wonder if we should add a general deduplication pass (maybe that is what you mean here?)

Particular j2cl pass I was referring to is the one that moves things out of to be eager globals. The deduplication is a separate optimization (that's part of the my large report that I shared earlier)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds good then, to keep the behavior in this PR, and consider the late deduplication pass. Now that we have a "late" stage that is safer than before, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok will do.

(Note that, I assume there was a typo though; the late pass will not be a late deduplication pass but the reverse conversion pass)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, I meant some kind of reverse conversion (it would have the effect of deduplicating bytes in code into a single copy in data).

if (size > MaxArrayNewFixedSize) {
return;
}
std::vector<Expression*> values;
values.reserve(size);
for (Index i = 0; i < size; i++) {
values.push_back(builder.makeConst(readElement(i)));
}
replaceCurrent(builder.makeArrayNewFixed(heapType, values));
}

void visitArrayGet(ArrayGet* curr) {
skipNonNullCast(curr->ref, curr);
trapOnNull(curr, curr->ref);
Expand Down
16 changes: 14 additions & 2 deletions src/wasm/literal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,23 @@ Literal Literal::makeNegOne(Type type) {
return makeFromInt32(-1, type);
}

Literal Literal::makeFromMemory(const void* p, Type type) {
Literal Literal::makeFromMemory(const void* p,
Type type,
std::optional<unsigned> byteSize) {
assert(type.isNumber());
auto size = byteSize.value_or(type.getByteSize());
assert(size == type.getByteSize() ||
(type == Type::i32 && (size == 1 || size == 2)));
switch (type.getBasic()) {
case Type::i32: {
return Literal(Bits::readLE<int32_t>(p));
switch (size) {
case 1:
return Literal(int32_t(Bits::readLE<int8_t>(p)));
case 2:
return Literal(int32_t(Bits::readLE<int16_t>(p)));
default:
return Literal(Bits::readLE<int32_t>(p));
}
}
case Type::i64: {
return Literal(Bits::readLE<int64_t>(p));
Expand Down
Loading
Loading