Skip to content

New pass: MultiHeap2Local - #9330

Merged
kripken merged 138 commits into
WebAssembly:mainfrom
kripken:psgs
Nov 3, 2026
Merged

kripken merged 138 commits into
WebAssembly:mainfrom
kripken:psgs

Conversation

@kripken

@kripken kripken commented Oct 30, 2026

Copy link
Copy Markdown
Member

This is a form of escape analysis, like Heap2Local, but it can handle multiple
allocations which Heap2Local can't, but otoh those must fit a simple, easy-to
identify pattern.

The pattern here is that while structs are allocated and used, all the uses
are simple struct.gets/sets. So we can just use the data in the fields
directly:

   x = new Foo(42);
   if (condition) x = new Foo(1337);
   else x.value = -1;
   bar(x.value, x.value);

=>

   x_value = (new Foo(42)).value;
   if (condition) x_value = (new Foo(1337)).value;
   else x_value = -1;
   bar(x_value, x_value);

We add a new x_value local here which stores the field, and write to
it from struct.new/struct.set, and read from it in struct.get. If those
are the only operations - no escape of the reference, and no ref.eq
etc. - then this is clearly valid to do. And after doing it, the allocations are
"untangled" - each is used just to write its fields, so their live ranges do
not overlap, and Heap2Local will remove them all. So this new pass,
when it optimizes, always leads to beneficial results, without tradeoffs.

This is a reboot of #9271, with the changes that it handles struct.sets,
and does not handle unknown values (the latter introduced unclear
tradeoffs). As a result the code in the pass is a lot simpler now.

This helps #8641 and almost fixes it. Specifically, once we merge
the two locals there, then this new pass will do all the rest. And we
should be getting those locals merged either through @stevenfontanella's
nullability pass, or my TypeGeneralizing pass.

Aside from helping that original testcase in #8641 (after I manually
merged the locals), this also helps 3/4 large wasm testcases I tried on,
and has no downside on 1/4.

kripken and others added 8 commits November 2, 2026 12:38
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Co-authored-by: Thomas Lively <tlively123@gmail.com>

@tlively tlively left a comment

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 would be good to see if we can get coverage for the remaining uncovered branches as well.

(func $optimize-struct-get-different-fields (param $x i32) (result i32)
;; Optimize when gets of $ref read different fields (field 0 and field 1).
(local $ref (ref null $struct-2fields))
(if

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.

Do all the tests need this if-then-else structure? It's definitely important to show we can handle it, but then for some of these other tests, I think a sequence of (drop (struct.get (local.get $x))) and (struct.set (local.get $x)...) would be sufficient.

I guess this is showing that Heap2Local benefits? Again, I think we can cover that in just a couple tests.

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.

A straightline sequence would have dead code, which I think makes it harder to read. That is, the if-else sequence makes it more realistic. Also more robust to possible future code changes.

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'm not sure what you mean about dead code. For example, couldn't this test be simplified to this?

  (func $optimize-struct-get-different-fields (param $x i32) (result i32)
    ;; Optimize when gets of $ref read different fields (field 0 and field 1).
    (local $ref (ref null $struct-2fields))
    (local.set $ref
      (struct.new_default $struct-2fields)
    )
    (drop
      (struct.get $struct-2fields 0
        (local.get $ref)
      )
    )
    (drop
      (struct.get $struct-2fields 1
        (local.get $ref)
      )
    )
  )

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.

You need two struct.news, though. Two subsequent ones are dead code.

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.

(We could drop the two struct.gets, I suppose, but it is shorter to do an i32.add as the test does, and more realistic?)

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.

Oh, because otherwise the optimization doesn't trigger? I don't think it's a problem to have dead code in a test case. It's much more concise than having all the if-else boilerplate, including the $x param, etc.

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.

Yes, we only optimize 2 struct.news (as Heap2Local handles the trivial case of 1, as discussed before).

Do you feel strongly here? I guess I don't, but otoh it does seem strictly better to me to have more realistic testcases, and also a variety of things in them, not all just being the absolute minimum for the current state of the code.

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 don't feel strongly enough to make you rewrite so many tests :)

)
)
(drop
(local.get $no)

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 is this different from the previous test with a dropped local.get? Is it just that there's also a local.tee in this one?

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.

This test has locals $yes and $no, and optimizes one and not the other. I'll add to the comment This tests that one local does not interfere with another.

@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

It would be good to see if we can get coverage for the remaining uncovered branches as well.

Where are you seeing that data? Is the new bot running somewhere I can read its output?

@tlively

tlively commented Nov 3, 2026

Copy link
Copy Markdown
Member

@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Right here in the diff, for example on https://github.com/WebAssembly/binaryen/pull/9330/changes#diff-a0afad61098fa88f25940c27395e9488baa969a1ff63f3d874322ff45164b60eR409.

Interesting, I see no coverage report there. Do I need to enable something?

@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Oh.. I have to scroll down and hunt for the coverage messages. Kind of inconvenient...

Index getFieldIndex(Expression* curr) {
if (auto* get = curr->dynCast<StructGet>()) {
return get->index;
} else if (auto* set = curr->dynCast<StructSet>()) {

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.

This is a false positive, as this code path is very much hit! Removing it reaches an abort.

return set->index;
} else {
WASM_UNREACHABLE("bad op");
}

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.

Also a false positive - that line is unreachable!

@kripken

kripken commented Nov 3, 2026 •

Copy link
Copy Markdown
Member Author

Added testcases for coverage. tbh I don't think they are really worth it... I guess coverage is good, but they are also trivially true in the code. Also there are 2 false positives from this coverage analysis.

@tlively

tlively commented Nov 3, 2026

Copy link
Copy Markdown
Member

There's also a side panel for the coverage annotations you can enable. It's not available at time of writing because the CI has not finished computing the coverage for the latest commits, but the button will be right next to the comment side panel button when it is available.

@tlively

tlively commented Nov 3, 2026

Copy link
Copy Markdown
Member

Added testcases for coverage. tbh I don't think they are really worth it... I guess coverage is good, but they are also trivially true in the code. Also there are 2 false positives from this coverage analysis.

I think we should feel free to not add tests when they don't make sense, rather than bending over backward to get to 100% coverage. I hope the annotations are still useful for finding missed edge cases. We should try to eliminate false positives, though. Do you remember what they were?

@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

I posted a comment next to each of the 2 false positives.

@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

(Looks like the 2 false positives vanished after a new code push, but the comments show which lines and what was going on.)

@tlively tlively left a comment

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.

LGTM!

@kripken
kripken merged commit e437de4 into WebAssembly:main Nov 3, 2026
18 checks passed
@kripken
kripken deleted the psgs branch November 3, 2026 22:23
@kripken kripken changed the title New pass: HeapUntangling New pass: MultiHeap2Local Nov 3, 2026
@kripken

kripken commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Oops, I forgot to update the title before merging, so the commit has the old name... but the code that landed has the updated name.

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.

2 participants