Skip to content

New pass: PropagateStructGets - #9271

Open
kripken wants to merge 76 commits into
WebAssembly:mainfrom
kripken:psgs
Open

kripken wants to merge 76 commits into
WebAssembly:mainfrom
kripken:psgs

Conversation

@kripken

@kripken kripken commented Oct 16, 2026

Copy link
Copy Markdown
Member

Our main escape analysis pass, Heap2Local, does not handle overlapping
live ranges, things like this:

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

More than one allocation is being read from in those x.value operations,
so Heap2Local sees the uses are not "exclusive", and it gives up.

Handling live ranges, sort of like liveness of locals for register allocation /
local coalescing, is possible, but quite complex. However, the pattern
above can be optimized in another way, which this new pass handles:
when the reference is always read from (using struct.get, which is the
.value operations above), then we can just shift from using the reference
to using data:

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

We store the data in a new local x_value, and that is all we need.

That is, this pass handles a situation that is more restricted than
Heap2Local, in that it doesn't do complex analysis of control flow
and handling of ref.eq and such, but it is more general in that it
can handle overlapping live ranges in a trivial way.

Fixes #8641 , a Dart testcase, and I saw this helps real-world binaries from
Kotlin and Java too - this is a common pattern.

@kripken
kripken requested a review from tlively October 16, 2026 20:36
@kripken
kripken requested a review from a team as a code owner October 16, 2026 20:36
Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp Outdated
Comment thread src/passes/PropagateStructGets.cpp Outdated
Comment thread src/passes/PropagateStructGets.cpp Outdated
// The parent struct.gets for each local index's gets.
std::unordered_map<Index, std::vector<StructGet*>> getParents;

// The parent struct.get for each tee.

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 briefly say what happens when a local.get or local.tee does not have a parent struct.get.

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.

Comments added.

Comment thread src/passes/PropagateStructGets.cpp Outdated
Comment on lines +124 to +126
// Avoid unreachable code, and leave atomic operations for now TODO
if (curr->type == Type::unreachable ||
curr->order != MemoryOrder::Unordered) {

@tlively tlively Oct 19, 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.

There should be no need to leave atomic struct.gets for later. If the accessed allocations don't escape, they can't be part of any synchronization between threads, so they can be optimized just like non-atomic accesses.

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.

Also, if we can optimize structs that don't come from allocations, this would still be valid when all their fields are immutable. Immutable fields cannot participate in cross-thread synchronization.

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 do optimize cases where structs arrive from e.g. parameters, i.e. not allocations that we view. See e.g. the test $set-unknown-immutable. Yes, that depends on immutability, as you said.

Is that enough for atomic synchronization? Would the struct.gets need to be atomic?

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, we can still optimize in that case. No, the struct.gets do not need to be atomic.

@kripken kripken Oct 20, 2026 •

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.

Is that because a Publish must happen after the immutable field is written at creation?

Added optimizing of atomic operations + tests.

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.

No, it's because cross-thread synchronization requires a release (or stronger) write on one thread that is then read from by an acquire (or stronger) read on another thread. The first half of that cannot happen with an immutable field. The initializing write done by struct.new and friends is not a release write.

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.

So other threads can read immutable fields non-atomically? Immutable writes (i.e. creations) are guaranteed to synchronize without atomics?

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, that pattern is safe due to the publish built into struct.new. But the synchronization effect of the publish does not extend to anything besides the fields of the particular published struct.

Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp
Comment thread test/lit/passes/propagate-struct-gets.wast
Comment thread src/passes/PropagateStructGets.cpp
Comment thread src/passes/PropagateStructGets.cpp
//
// If we replace `l = x` with `l_value = x.value` on the first line, then
// we need it to remain the same until the return at the end. If x can be
// written to, that is a danger. Note how this cannot happen if each set

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.

set -> local.set

Comment thread src/passes/PropagateStructGets.cpp
;; H2L-NEXT: )
(func $struct.get-atomic-param (param $x i32) (param $p (ref $struct)) (result i32)
(local $ref (ref null $struct))
;; Now we do not see one set, which writes a param. But the field is

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 not see one set" => "have something other than an allocation flow into a local.set"

(and in the next test)

This branch has not been deployed

No deployments
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.

Optimization issue where allocation is not eliminated

2 participants