Skip to content

Use fewer scratch locals in IRBuilder#8608

Merged
tlively merged 2 commits intomainfrom
irbuilder-greedy
Apr 16, 2026
Merged

Use fewer scratch locals in IRBuilder#8608
tlively merged 2 commits intomainfrom
irbuilder-greedy

Conversation

@tlively
Copy link
Copy Markdown
Member

@tlively tlively commented Apr 15, 2026

When popping children for an expression, greedily pop none-typed expressions below the last value-producing expression in the stack, packaging all the popped instructions into a new block. This avoids leaving the none-typed expressions on top of the stack, which is good because having them on top of the stack would force the use of a scratch local when the next operand is popped.

Besides producing better IR with fewer scratch locals, this also has the benefit of better round-tripping IR through binaries. The binary writer has an optimization where it will elide unnamed blocks because they cannot possibly be branch targets, but this could create "stacky" code that would previously introduce a scratch local when it was parsed back into IR. Now IRBuilder just recreates exactly the unnamed block that had been present in the IR in first place.

While we don't generally guarantee that we can perfectly round-trip IR through binaries, this reduces the number of cases where round-trips lead to increased code size.

Fixes #8413.

When popping children for an expression, greedily pop none-typed expressions below the last value-producing expression in the stack, packaging all the popped instructions into a new block. This avoids leaving the none-typed expressions on top of the stack, which is good because having them on top of the stack would force the use of a scratch local when the next operand is popped.

Besides producing better IR with fewer scratch locals, this also has the benefit of better round-tripping IR through binaries. The binary writer has an optimization where it will elide unnamed blocks because they cannot possibly be branch targets, but this could create "stacky" code that would previously introduce a scratch local when it was parsed back into IR. Now IRBuilder just recreates exactly the unnamed block that had been present in the IR in first place.

While we don't generally guarantee that we can perfectly round-trip IR through binaries, this reduces the number of cases where round-trips lead to increased code size.

Fixes #8413.
@tlively tlively requested a review from a team as a code owner April 15, 2026 22:37
@tlively tlively requested review from aheejin, kripken and stevenfontanella and removed request for a team April 15, 2026 22:37
@@ -1,44 +0,0 @@
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
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.

Why remove this file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Round-tripping no longer introduces a scratch local, so it no longer tests anything interesting.

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.

It could test that we do not introduce a scratch local?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That behavior is already well tested in e.g. wat-kitchen-sink.wast. There's nothing special about the --roundtrip pass to test here.

Comment thread src/wasm/wasm-ir-builder.cpp Outdated

if (type.size() == sizeHint || type.size() <= 1) {
if (hoisted.get) {
if (hoisted.valIndex < scope.exprStack.size() - 1) {
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.

Why is this correct?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Basically we need to packageAsBlock whenever we want to treat multiple expressions as a unit, rather than just using the expression on top of the stack. This used to be the case precisely when we also had a get of a scratch local. Now it's possible for the value-producing expression to be on top of the stack, so we don't need a scratch local, but for us to greedily be consuming other expressions as well, so we still need a block. We handle this by more explicitly checking whether the hoisted index is something other than the top of the stack.

(I'll rename it from valIndex, since that name no longer gives the right idea.)

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.

Makes sense, thanks!

Copy link
Copy Markdown
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

Nice idea!

Copy link
Copy Markdown
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

(I don't feel strongly about the removed file)

Comment thread src/wasm/wasm-ir-builder.cpp Outdated

if (type.size() == sizeHint || type.size() <= 1) {
if (hoisted.get) {
if (hoisted.valIndex < scope.exprStack.size() - 1) {
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.

Makes sense, thanks!

@tlively
Copy link
Copy Markdown
Member Author

tlively commented Apr 15, 2026

@kripken, the clusterfuzz test failure here does not look related to this change. Is there a better way to investigate than just fuzzing locally and seeing if something similar shows up again?

https://github.com/WebAssembly/binaryen/actions/runs/24481972445/job/71548381989?pr=8608#step:10:14071

@kripken
Copy link
Copy Markdown
Member

kripken commented Apr 15, 2026

I don't think there's a way to download the test directory from CI, unfortunately. Sometimes the error itself has been useful in guiding me to what could be the issue. Otherwise I would ignore this.

@tlively tlively merged commit 4301eae into main Apr 16, 2026
16 checks passed
@tlively tlively deleted the irbuilder-greedy branch April 16, 2026 00:10
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.

Re-saving a .wasm file with wasm-opt -g results in a code size regression

2 participants