Skip to content

JIT: treat throwing trees as GC safe points - #135970

Open
lewing wants to merge 8 commits into
dotnet:mainfrom
lewing:lewing-wasm-stale-dynamichelperframe
Open

lewing wants to merge 8 commits into
dotnet:mainfrom
lewing:lewing-wasm-stale-dynamichelperframe

Conversation

@lewing

@lewing lewing commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

optRemoveRedundantZeroInits skips prolog zeroing for a GC local when no GC safe point precedes its first store. IsPotentialGCSafePoint ignored trees that may throw, but they can call a throw helper, which is a GC safe point. An uninitialized GC slot could then be reported:

  • Wasm reports all GC locals on the frame as untracked. In Object.GetType, the null check on this calls ThrowNullReferenceException before its GC temps are stored. A GC there reported stale stack contents and TypeIntrinsics_r asserted in pMT->Validate().
  • Other targets hit it with untracked (e.g. address-exposed) GC locals stored after a bounds check.

IsPotentialGCSafePoint now returns true for GTF_EXCEPT trees on all targets.

The analysis still runs before helper expansion and lowering, so it can't see every safe point; doing it closer to codegen is separate work.

Diffs

SuperPMI (benchmarks, realworld, aspire):

Target Contexts with diffs Bytes
osx-arm64 70 +204 / -24
windows-x64 99 +697 / -20
linux-x64 97 +553 / -58

System.Private.CoreLib.wasm: +6,944 bytes (0.02%).

Validation

Run main This PR
Runtime_135966, osx-arm64 Checked, GCStress=0/1/4 segfault pass
JIT_r (browser-wasm R2R Checked, CI Helix payloads), GCStress=1 pMT->Validate() assert, 3/3 pass, 3/3
Runtime_135966, browser-wasm R2R, GCStress=0/1 assert pass

Resolves #135966

Note

This PR description was drafted with GitHub Copilot.

lewing and others added 3 commits October 16, 2026 13:40
…oval

optRemoveRedundantZeroInits marks a GC local as explicitly initialized, so the
prolog doesn't zero it, when no GC safe point precedes its first store. That's
sound where tracked locals are only reported while live, but wasm reports every
GC slot on the frame for the whole method, including aborted frames during
exception dispatch. Wasm also has no hardware exceptions: a null check or other
throwing tree becomes a call to a throw helper.

So a GC ref local stored after a null check (e.g. Object.GetType's temps) was
reported with whatever the stack held when the check threw, which failed
pMT->Validate() in TypeIntrinsics_r on the browser-wasm R2R leg.

On wasm, IsPotentialGCSafePoint now treats trees with GTF_EXCEPT as safe points.
System.Private.CoreLib.wasm grows by 6,944 bytes (0.02%).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
@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 16, 2026
@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.

@jkotas jkotas added the arch-wasm WebAssembly architecture label Oct 16, 2026
@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.

@lewing

lewing commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

The native whole appears to be real too. Will update after validating.

@EgorBo

EgorBo commented Oct 16, 2026

Copy link
Copy Markdown
Member

optRemoveRedundantZeroInits marks a GC local as explicitly initialized

I think then it shouldn't do it. We already suspect that optRemoveRedundantZeroInits doesn't have enough knowledge to say that there is no gc safe point between prolog and the first local initialization. if the impact is not huge, I would rather remove that optimization, at least for wasm

…l targets

Move the fix from IsPotentialGCSafePoint into optRemoveRedundantZeroInits and
make it target-independent. A GC local reported as an untracked slot is
reported whenever its frame is, including at a throw helper call that a tree
before the local's first store may make. Natively, an address-exposed object
local stored after a bounds check was reported uninitialized at the
RNGCHKFAIL call, and the test now crashes without the fix on osx-arm64 even
without GC stress. Wasm reports every GC local on the frame as untracked, so
its codegen is unchanged from the previous commits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
@lewing lewing changed the title JIT: treat throwing trees as GC safe points on wasm for zero-init removal JIT: zero-init untracked GC locals stored after a throwing tree Oct 16, 2026
@lewing

lewing commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

@EgorBo agreed. The latest push (e8083cd) still relies on that safe-point analysis, just with throwing trees added. That push also covers a native case of the same hole, for untracked locals; the repro and SuperPMI diffs are in the description.

I'm switching to not marking untracked GC locals (all GC locals on wasm) as explicitly initialized at all, without relying on the safe-point scan. I'm implementing and testing that now, and will post SuperPMI and System.Private.CoreLib.wasm size diffs once they're measured.

Note

This comment was drafted with GitHub Copilot.

Comment thread src/coreclr/jit/optimizer.cpp Outdated
Instead of extending the safe-point scan with throwing trees, don't mark GC
locals reported as untracked slots as explicitly initialized at all. They
are reported whenever their frame is, and optRemoveRedundantZeroInits runs
before later phases (helper expansion, lowering) that can still introduce
calls, so it can't rule out every gc-safe point before the store. Tracked
GC locals are only reported where they're live and keep the optimization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
@lewing lewing changed the title JIT: zero-init untracked GC locals stored after a throwing tree JIT: never skip prolog zeroing for untracked GC locals Oct 16, 2026
@lewing

lewing commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

@EgorBo updated in 8f3d1ef. optRemoveRedundantZeroInits no longer marks GC locals reported as untracked slots (all GC locals on the frame on wasm, untracked ones elsewhere) as explicitly initialized at all. Tracked GC locals keep the optimization.

Impact:

  • SuperPMI (benchmarks, realworld, aspire): osx-arm64 has 277 of 299k contexts with diffs (+928 bytes), windows-x64 351 of 267k (+2,533/-5), linux-x64 329 of 512k (+2,238/-18). Each diff is one added prolog zeroing store.
  • System.Private.CoreLib.wasm grows by 14,208 bytes (+0.046%).
  • Runtime_135966 fails on main (wasm, and osx-arm64 even without GC stress) and passes with this change. JIT_r passes under GCStress=1 on wasm.

Details are in the description.

Note

This comment was drafted with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
@lewing

lewing commented Oct 19, 2026

Copy link
Copy Markdown
Member Author

The wasm side of this is getting hit fairly frequently if would be good to get it a review.

@lewing

lewing commented Oct 21, 2026 •

Copy link
Copy Markdown
Member Author

@EgorBo if you prefer, I can only modify wasm and add and activeissue to the test for the other platforms?

@EgorBo

EgorBo commented Oct 21, 2026

Copy link
Copy Markdown
Member

@lewing I thought about this for a bit and realized that your initial fix was correct, you did

if ((tree->gtFlags & GTF_EXCEPT) != 0)
{
    return true;
}

in IsPotentialGCSafePoint only for WASM, but we need to do it for all platforms, because any exception acts as a gc safe point basically and we shouldn't have non-initialized GC slots on any platform, can you revert back to that (but without ifdef WASM)

@EgorBo

EgorBo commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

This check is still potentially incorrect, but that needs a rewrite to do this analysis closer to codegen, because it's too early in the optimization pipeline to guess whether there will be gc safe points between two points or not.

Per review, go back to counting trees with GTF_EXCEPT as potential GC safe
points in IsPotentialGCSafePoint, without limiting it to wasm: a tree that
may throw can call a throw helper, which is a GC safe point on every target.
This replaces the untracked-local special case in optRemoveRedundantZeroInits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 45f42362-f012-4030-8643-26f4c1fbf2e1
@lewing lewing changed the title JIT: never skip prolog zeroing for untracked GC locals JIT: treat throwing trees as GC safe points Oct 21, 2026
@lewing

lewing commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Done in 0e216df, without the ifdef. Diffs are smaller than the untracked-only version: 70-99 contexts per target (+204 to +697 bytes), CoreLib.wasm +6,944 bytes. Runtime_135966 and JIT_r under GCStress pass on osx-arm64 and browser-wasm.

Note

This comment was drafted with GitHub Copilot.

@lewing
lewing enabled auto-merge (squash) October 21, 2026 21:35

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

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.

[wasm/R2R] TypeIntrinsics_r/_ro: GC reports uninitialized frame slots during throw-helper calls (pMT->Validate assert)

3 participants