Skip to content

[wasm] Regalloc: Allow Non-exposed GC Ref Locals to Live in Wasm Locals - #135543

Merged
adamperlin merged 8 commits into
dotnet:mainfrom
adamperlin:adamperlin/wasm-enregister-gcsafe-refs
Oct 21, 2026
Merged

adamperlin merged 8 commits into
dotnet:mainfrom
adamperlin:adamperlin/wasm-enregister-gcsafe-refs

Conversation

@adamperlin

@adamperlin adamperlin commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Background

This PR introduces a pass which does some liveness analysis to determine GC-ref tracked locals which are meaningfully live at GC safepoints. Locals which are live at safepoints are treated as before and we continue to assign them pinned stack homes. However, the pass uses this list to then determine candidate gc ref locals which are NOT live at any safepoint, which RA can assign local homes since the GC does not need to see them.

The one exception here is temporary reads of locals. A tree temporary from a local var which is live across a call still needs a pinned stack slot, and `fgWasmSpillRefs has been updated to handle this case. This is also why this analysis was structured as a separate phase; it is a self-contained analysis and provides information that two subsequent phases need to consume.

Notes

For throwing code paths, we must account for any LIR node which could possibly throw (this includes all null checks, div-by-zero checks, range checks, etc.) and also track which live locals are actually live on the throwing path. A reference stored in a local is not actually exposed to GC if the only collecting path is the throwing path, unless it is also live into a handler for that site.

Performance

SPMI

Diff: -549,758/+1,245 bytes
16,424 size improvements, 205 size regressions
Total bytes of delta: -548,513 (-6.41 % of base)

Local benchmarks

Metric Mono baseline (interpreter) CoreCLR baseline (composite R2R) CoreCLR local (composite R2R) Local vs. CoreCLR baseline Local vs. Mono baseline
Cold startup — mobile 3,320 ms 7,228 ms 6,539 ms 9.5% faster 97.0% slower
Walkthrough — desktop 9.23 s 5.95 s 5.56 s 6.6% faster 39.8% faster
Cold download — desktop 4.567 MB 10.323 MB 10.138 MB 1.8% smaller 122.0% larger
App publish 37.85 s 151.89 s 147.84 s 2.7% faster 290.6% slower

Validation

The new WasmGCLiveness tests pass which is a good start, but it is hard to force the GC to move objects to detect if something is wrong. If we had GC Stress for Wasm that might help. For several of the test cases (including live across finally/EH, mixed stack/reg homes, and others), I inspected JIT dumps manually to ensure that the pass was working as expected.

@adamperlin
adamperlin requested a balanced review from Copilot October 9, 2026 21:35
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Oct 9, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 6 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The tests do not automatically verify that eligible references are actually assigned Wasm-local homes.

3 open findings
What changed in this PR

Adds Wasm GC-reference liveness analysis so safe reference locals can use Wasm-local homes.

Changes:

  • Identifies tracked GC references not live across safepoints.
  • Enables their register allocation and preserves call arguments via spills.
  • Adds GC-liveness correctness scenarios.
File Description
src/​coreclr/​jit/​fgwasm.cpp Implements candidate analysis and spill handling.
src/​coreclr/​jit/​regallocwasm.cpp Allows identified references into Wasm locals.
src/​coreclr/​jit/​compiler.cpp Runs the new analysis phase.
src/​coreclr/​jit/​compiler.h Declares candidate state and phase method.
src/​coreclr/​jit/​compphases.h Registers the new phase.
src/​coreclr/​jit/​compmemkind.h Adds candidate-set allocation tracking.
src/​tests/​JIT/​Directed/​WasmGCLiveness/​WasmGCLiveness.cs Adds GC-liveness scenarios.
src/​tests/​JIT/​Directed/​WasmGCLiveness/​WasmGCLiveness.csproj Configures the isolated priority-one test.

🧠 Review effort: Balanced

Comment thread src/coreclr/jit/regallocwasm.cpp
Comment thread src/coreclr/jit/fgwasm.cpp Outdated
Comment thread src/tests/JIT/Directed/WasmGCLiveness/WasmGCLiveness.cs Outdated
adamperlin added a commit that referenced this pull request Oct 15, 2026
The current `fgWasmSpillRefs` pass spills all outstanding GC-ref typed
temporary values, even if those temps are consumed by the most immediate
call. However, for calls it should be the callee's responsibility to
maintain GC safety of any incoming GC-ref typed args (either by spilling
to a pinned stack home, spilling and reloading, or potentially avoiding
spilling at all, see #135543). In the R2R fixup case, where a thunk will
fix up the call target on first invocation, we should also be
GC-protecting all incoming args by default (See the spilling logic in
crossgen2 `WasmImportThunk.cs`).

This PR removes the need for spilling in cases like:
```
MyClass x = Allocate();
Use(x);
``` 

Note that the spilling STILL needs to happen for nested calls, where a
ref has an outstanding use which is NOT its most immediate parent.
```csharp
MyClass x = Allocate();
M(x, F()) // <-- temporary value of x is an operand of the call to M, and may be live across the other sub-tree call, F where a GC might occur.
```
## Results
### SuperPMI
Total bytes of base: 8771651
Total bytes of diff: 8555629
Total bytes of delta: **-216,022 (-2.46% of base)**
### Local Benchmarks
| Metric | CoreCLR baseline | CoreCLR after | Change |
|---|---:|---:|---:|
| Mobile cold startup | 7,474 ms | 6,901 ms | 7.7% faster |
| Desktop walkthrough | 5.60 s | 6.05 s | 8.1% slower |
| Desktop cold download | 10.324 MB | 10.215 MB | 1.1% smaller |
| Havit publish | 150.81 s | 149.21 s | 1.1% faster |
Cold download and startup improve as expected. I didn't put too much
weight on the walkthrough benchmark because it is only run once and is
pretty variable.
@adamperlin
adamperlin force-pushed the adamperlin/wasm-enregister-gcsafe-refs branch from 6ee0537 to f702924 Compare October 15, 2026 20:43
@adamperlin
adamperlin marked this pull request as ready for review October 16, 2026 00:33
@adamperlin

adamperlin commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

The regressions have mostly been mitigated by #135577, there were many more before. The ones that remain are expected given the current state; In these cases we have a local which is not considered live over a safepoint using local liveness, but its value is read into a temporary which is live across a safepoint. In that case, it is a bit more expensive to spill the temporary value and reload it than it would have been to assign the local a stack home to begin with. We could potentially try and do some more analysis to avoid even these cases, but that doesn't seem worth it to me due to the comparatively very small size of the regressions.

@adamperlin

adamperlin commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

cc @dotnet/wasm-contrib; @EgorBo PTAL.

@EgorBo EgorBo 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.

Approving to unblock the work, but this might need a 2nd look by @AndyAyersMS I think

@pavelsavara

Copy link
Copy Markdown
Member

would this make the values/references invisible to managed debugger ? Is that OK for R2R code ? @radekdoulik

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

…r a method and checks the state of local liveness

at each potential GC safepoint to determine which locals are live across a safepoint.
It distinguishes between normal GC safepoints triggered by a call or and exception-only GC safepoints, where an operation can only collect on its throwing path.
This allows us to effectively discount locals as gc-exposed if they are only exposed to GC on a throwing path but are NOT live into
a handler.

The set of gc-safe locals is the set of gc exposed locals subracted from the set of all locals. This reduced set of gc-safe locals
can be used by Wasm regalloc to make gc refs into wasm-local candidates.
…asses to allow safely placing select GC references in locals
@adamperlin
adamperlin force-pushed the adamperlin/wasm-enregister-gcsafe-refs branch from e3d52f9 to 4e7a64c Compare October 20, 2026 20:34
@adamperlin

Copy link
Copy Markdown
Contributor Author

/ba-g unrelated failures

@adamperlin
adamperlin merged commit dedc9b3 into dotnet:main Oct 21, 2026
131 of 135 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants