Skip to content

JIT: raise the cost of ReadyToRun generic lookups - #136731

Merged
lewing merged 6 commits into
mainfrom
lewing-wasm-forward-single-use-local-copies
Nov 13, 2026
Merged

lewing merged 6 commits into
mainfrom
lewing-wasm-forward-single-use-local-copies

Conversation

@lewing

@lewing lewing commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Loop hoisting only hoists a tree from a block that doesn't run on every iteration when it costs at least IND_COST_EX * 16. READYTORUN_GENERIC_HANDLE calls are costed like any other call, although each one calls a helper that performs a generic dictionary lookup, so loop-invariant lookups stay in the loop. SpanHelpers.IndexOfAny<__Canon> makes two lookup calls per iteration in browser-wasm R2R code. This raises their cost.

The helper is modeled as non-throwing. That holds for CoreCLR, which already reports type-load failures early for unshared code (at JIT time or in R2R method-entry fixups). NativeAOT instead compiles lookups it couldn't build into helpers that throw when called, so on NativeAOT the call is now reported as throwing, for side effects, hoisting and value numbering. The exception's value number is derived from the lookup, so identical lookups still CSE. SuperPMI shows no NativeAOT diffs.

Measured on browser-wasm R2R (Checked runtime), IndexOfAny<string> is 2–4% faster; osx-arm64 Release shows no measurable change.

Resolves #136723

Note

This PR description was generated with GitHub Copilot.

…locks

Loop hoisting only hoists trees from blocks that are not executed on every
iteration when their cost is at least IND_COST_EX * 16. Runtime lookup helper
calls (CORINFO_HELP_READYTORUN_GENERIC_HANDLE, CORINFO_HELP_RUNTIMEHANDLE_*)
are costed like cheap calls, so loop-invariant generic dictionary lookups in
such blocks stayed in the loop.

Exempt these helpers from the speculative cost gate outside of NativeAOT.
They are modeled as pure and non-throwing, and CoreCLR already reports type
load failures early for unshared code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@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 30, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 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.

…erCall

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/coreclr/jit/optimizer.cpp Resolved
…isting gate

Replace the hoisting exemption with a higher cost for
CORINFO_HELP_READYTORUN_GENERIC_HANDLE calls outside of NativeAOT. JIT-mode
RUNTIMEHANDLE_* lookups are no longer affected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing lewing changed the title JIT: hoist generic runtime lookups from conditionally executed loop blocks JIT: raise the cost of ReadyToRun generic lookups Oct 30, 2026
@lewing

lewing commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

On targets with a JIT, hot R2R methods tier up, so this mostly affects startup code. Locally, the final Tier1 IndexOfAny<__Canon> is identical with and without the change, and osx-arm64 Release shows no measurable difference. On browser, WASI, iOS, tvOS and MacCatalyst the R2R code is the steady-state code, which is where the 2–4% wasm gain comes from.

crossgen2 already knows which case it's compiling for (GetTargetAllowsRuntimeCodeGeneration in crossgen2's Program.cs), but that isn't passed to the JIT. A JIT flag for it would let this cost bump apply only where R2R code is final. That would keep the crossgen2 PerfScore regressions on arm64/x64 out of the picture, and give a place for other final-code R2R tuning, e.g. the VIRTUAL_FUNC_PTR cost noted in #136723. Since it's a JIT-EE interface change, I'd do it separately unless reviewers prefer it here.

Note

This comment was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing
lewing marked this pull request as ready for review November 1, 2026 03:33
Comment thread src/coreclr/jit/gentree.cpp Resolved
NativeAOT compiles generic lookups it failed to build into helpers that throw
when invoked, so CORINFO_HELP_READYTORUN_GENERIC_HANDLE is not exception-free
there. Report it as throwing through GenTreeCall::CallExceptions, and use that
for side effects, hoisting and value numbering. Value numbering gives the
exception a value determined by the lookup, so identical lookups can still be
CSEd.

With the exception modeled, the lookup cost no longer needs an ABI check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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

Instead of introducing a new CallExceptions can't we just fold the NAOT behavior into the existing helper call properties?

Move the NativeAOT special case out of GenTreeCall::CallExceptions into the
helper call property table. HelperCallProperties now keeps a NativeAOT
exception set alongside the default one, and NoThrow/ThrownExceptions take
the ABI. Consumers query the table directly again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

@AndyAyersMS done in db4999f. HelperCallProperties now keeps a NativeAOT exception set next to the default one. init() marks READYTORUN_GENERIC_HANDLE as throwing there, and NoThrow/ThrownExceptions take the ABI. The table is a static built before any compile, so the ABI has to come from the caller. CallExceptions is back to a plain table lookup, and side effects, hoisting and VN query the table directly again. Local SuperPMI matches the previous revision: no NativeAOT diffs, same crossgen2 diffs.

Note

This reply was generated with GitHub Copilot.

@lewing
lewing merged commit a0fecfe into main Nov 13, 2026
125 of 128 checks passed
@lewing
lewing deleted the lewing-wasm-forward-single-use-local-copies branch November 13, 2026 04:36
Comment thread src/coreclr/jit/utils.cpp
isPure = true;
exceptions = ExceptionSetFlags::None;
nonNullReturn = true;
// NativeAOT compiles generic lookups it failed to build into helpers that throw when invoked.

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.

This does not look accurate. Regular CoreCLR can throw from this helper as well, for similar reasons as NAOT. This helper calls TypeLoader that can fail for myriad of reasons.

What is this special casing trying to achieve? What would break if the helper is annotated same way on both runtimes?

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.

[JIT] R2R generic runtime lookups (CORINFO_HELP_READYTORUN_GENERIC_HANDLE) are not hoisted out of loops

4 participants