Repository navigation
Lower constant array.new_data in OptimizeInstructions #9248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b5fc51d
e76189f
48a748a
d702d51
c15f70d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()) { | ||
| // 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about putting this behind
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.