Repository navigation
Conversation
| // 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. |
There was a problem hiding this comment.
It would be good to briefly say what happens when a local.get or local.tee does not have a parent struct.get.
| // Avoid unreachable code, and leave atomic operations for now TODO | ||
| if (curr->type == Type::unreachable || | ||
| curr->order != MemoryOrder::Unordered) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Yes, we can still optimize in that case. No, the struct.gets do not need to be atomic.
There was a problem hiding this comment.
Is that because a Publish must happen after the immutable field is written at creation?
Added optimizing of atomic operations + tests.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
So other threads can read immutable fields non-atomically? Immutable writes (i.e. creations) are guaranteed to synchronize without atomics?
There was a problem hiding this comment.
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.
| if (teeIt == teeParents.end()) { | ||
| // This tee does not feed into a struct.get, so the allocation(s) | ||
| // might escape. | ||
| ok = false; | ||
| break; |
There was a problem hiding this comment.
This also could have been checked directly via an ExpressionStackWalker rather than after the fact.
| // Each used field's value must not change between the local.set and | ||
| // local.get: either the field is immutable, or every set writes a fresh | ||
| // struct.new that only ever flows into this local's struct.gets. (Note |
There was a problem hiding this comment.
How could the field's value change, given that we have ruled out struct.sets? Also, it is not clear which local.set and local.get this refers to.
There was a problem hiding this comment.
Gemini says we've only ruled out struct.sets through this particular local, but aliasing could allow other sets to slip through. It would be good to clarify that in the comment.
| // bar(x_value, x_value); | ||
| // | ||
| // Note how the `.value` (the struct.get) happens *before* `foo()`, | ||
| // after the optimization, so we may have moved a trap around here. It is |
There was a problem hiding this comment.
Don't we know the local.set values always come from allocations, though?
There was a problem hiding this comment.
Gemini says the pass also supports structs coming from parameters or calls as long as their fields are all immutable. It would be good to update the top-level comment to reflect that.
There was a problem hiding this comment.
Added to the top-level comment to clarify that.
There was a problem hiding this comment.
Though this does make me think, should we optimize cases without any allocation? atm we do, and it is not obvious to me if that is helpful or not.
|
|
||
| void visitTry(Try* curr) { hasTry = true; } | ||
|
|
||
| void visitFunction(Function* func) { |
There was a problem hiding this comment.
It would be nice to break this up into helper functions. It's huge.
There was a problem hiding this comment.
Split up into find-locals-to-optimize and optimize functions. I didn't see other obvious opportunities to split stuff up, as the rest would require a bunch of outparams etc.
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Co-authored-by: Thomas Lively <tlively123@gmail.com>
Our main escape analysis pass, Heap2Local, does not handle overlapping
live ranges, things like this:
More than one allocation is being read from in those
x.valueoperations,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.valueoperations above), then we can just shift from using the referenceto using data:
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.eqand such, but it is more general in that itcan 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.