Skip to content

JIT: Devirtualize calls before probing them in optimized instrumented code - #136100

Merged
JulieLeeMSFT merged 1 commit into
dotnet:mainfrom
JulieLeeMSFT:fix-135557-badinlinee
Oct 21, 2026
Merged

JulieLeeMSFT merged 1 commit into
dotnet:mainfrom
JulieLeeMSFT:fix-135557-badinlinee

Conversation

@JulieLeeMSFT

Copy link
Copy Markdown
Member

Fixes #135557

JIT/opt/GuardedDevirtualization/badinlinee fails in the rolling runtime-coreclr jitstress pipeline (instrument_if_optimizing, jitstress1, jitstress1_tiered, jitstress_random_*, jitstress_isas_1_*) with Test allocated 24000 bytes. I reproduced it locally (windows-x64 Checked) and found three independent root causes. This PR fixes each of them instead of disabling the test.

1. JIT: optimized instrumented code never devirtualized the calls it probed (instrument_if_optimizing)

impImportCall called impConsiderCallProbe before trying to devirtualize and skipped impDevirtualizeCall for every call it marked for a class/method probe (there was a TODO: "if we can devirtualize we should be able to suppress the probe"). The instrumentor then spills this into a temp without class information, so late devirtualization fails (no type available (op=LCL_VAR)), the inliner reports target not direct, and in new Base().M() the allocation stays because the object is also passed to the CORINFO_HELP_CLASSPROFILE32 helper. This hits every optimized+instrumented compile: OSR with PGO, hot R2R methods promoted to the instrumented tier 1, TieredPGO_InstrumentedTierAlwaysOptimized, JitInstrumentIfOptimizing.

The fix tries to devirtualize first and only probes the calls that are still virtual afterwards. A call that is going to be probed still cannot become a guarded devirtualization candidate (the probe info and the candidate info share storage in GenTreeCall), so considerGuardedDevirtualization now bails out for such calls, which keeps the previous behavior there. impConsiderCallProbe no longer returns a value because nothing uses it anymore. Non-instrumented compiles are not affected.

Test() in the instrumented tier 1 goes from a virtual call plus allocation (61 bytes) to mov eax, 42; ret.

2. badinlinee cannot hold under JitStress

JitStress picks methods by hash: at DOTNET_JitStress=1 it selects Test for STRESS_MIN_OPTS (and for the forced NO_WAY fallback of STRESS_GENERIC_VARN, which also retries the compile with MinOpts), so the final tier is MinOpts, nothing is inlined and the allocation stays. Disabling only STRESS_MIN_OPTS is not enough, so the test now uses [SkipOnCoreClr(..., RuntimeTestModes.JitStress)] like other tests that check optimization results under JitStress (it needs a TestLibrary reference for that).

3. badinlinee depended on wall-clock timing

The old warm-up was a fixed 100 x (100 calls + Sleep(15)) per method. With TC_CallCountThreshold=1 the tier-up queue holds hundreds of CoreLib methods, so Poison (which needs two promotions) usually never reached tier 1 before the second phase started, i.e. the GDV scenario of #133993 was not exercised at all: re-introducing that bug (reporting the GDV inline failure against call->gtCallMethHnd) still passed the old test 3/3. It also failed with DOTNET_ForceRelocs=1 (the JIT does twice the work) because Test was still in tier 0. The test now keeps calling the method until JitInfo.GetCompiledMethodCount() has been unchanged for 20 iterations, first for Poison, then for Test, and asserts with Assert.Equal instead of printing and returning -1. With the re-introduced #133993 bug the new test fails 3/3 in the normal scenario.

New regression test

src/tests/JIT/Regression/JitBlue/Runtime_135557 is a minimal repro of (1) that does not depend on the GDV scenario: new Base().M() with DOTNET_JitInstrumentIfOptimizing=1 (Checked/Debug JIT only, ignored by a Release JIT) must not allocate once Test reaches its final tier. 10/10 runs fail with the old JIT (Expected: 0 Actual: 24000) and 10/10 pass with the fix.

Testing

  • badinlinee and Runtime_135557 on windows-x64 Checked. I ran badinlinee in 49 scenarios of testenvironment.proj with the old and the new JIT. Old JIT with the old test: fails forcerelocs, jitstress1, jitstress1_tiered, jitstress_isas_1_* and instrument_if_optimizing. Old JIT with the new test: only instrument_if_optimizing fails. New JIT with the new test: everything passes. jitpartialcompilation_pgo_stress_random is excluded from these statements: it crashes (exit code 0x80000003) identically with the old JIT and the old test and is unrelated (other tests pass in that scenario).
  • All of src/tests/JIT (priority 1, 22,288 tests, including the two changed ones) with the new JIT in the default configuration and in the instrument_if_optimizing and fullpgo_methodprofiling_always_optimized scenarios, which instrument every optimized compile and so exercise the new path heavily: 22,125 passed, 0 failed, 163 skipped, 2 active issues in each of them.
  • SuperPMI (windows x64, Checked JIT): replay is clean on the standard collections. Replaying coreclr_tests.run reports missing data for 8179 of 620965 contexts, all ResolveVirtualMethod, i.e. the new JIT-EE query in optimized+instrumented compiles (the old JIT replays this collection clean). Asm diffs against the base JIT on benchmarks.run, benchmarks.run_pgo, benchmarks.run_pgo_optrepeat, coreclr_tests.run, libraries.crossgen2, libraries.pmi and realworld.run (1,466,464 contexts): no diffs, which confirms that non-instrumented compiles are unchanged. To see the actual effect I collected 15,616 contexts by running crossgen2 on CoreLib with TieredPGO and TieredPGO_InstrumentedTierAlwaysOptimized (5,153 of them Instrumented Tier1): clean replay and 45 diffs (8 size improvements, 37 regressions), +2,420 bytes (+0.04%) overall, all in instrumented tier 1 compiles that now devirtualize and inline calls with a known receiver.
  • jitformat.py reports no formatting changes.

Note

This pull request description was generated with GitHub Copilot.

… code

In optimized code that is also instrumented (OSR with PGO, hot R2R methods
promoted to the instrumented tier 1, TieredPGO_InstrumentedTierAlwaysOptimized,
JitInstrumentIfOptimizing), impImportCall decided to probe a virtual call
before trying to devirtualize it and never devirtualized probed calls. Even
when the exact type of the receiver was known the call stayed virtual, could
not be inlined and kept the object alive for the class probe helper.

Try to devirtualize first and only probe the calls that are still virtual.
Calls that are going to be probed still can't become guarded devirtualization
candidates because the probe info and the candidate info share storage in the
call node, so considerGuardedDevirtualization bails out for them like before.

Make the JIT/opt/GuardedDevirtualization/badinlinee test robust, it failed in
the jitstress pipeline for this and for two other reasons:
- JitStress randomly switches methods to MinOpts, which keeps the allocation
  the test checks, so skip the test under JitStress.
- The fixed loop counts were not enough for Poison and Test to reach their
  final tier when tiering is slow (for example with ForceRelocs), which also
  left the guarded devirtualization scenario untested. Wait until the JIT is
  idle instead.

Add Runtime_135557, which checks that the allocation in new Base().M() goes
away in an instrumented final tier.

Fixes dotnet#135557

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ea5600e2-9176-4e98-9306-8487dd7cbb5f
@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 19, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 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.

@EgorBo

EgorBo commented Oct 19, 2026

Copy link
Copy Markdown
Member

@JulieLeeMSFT

Copy link
Copy Markdown
Member Author

test failure might be related - fgInline throws an exception

The test failure looks like #136020.

@EgorBo
EgorBo enabled auto-merge (squash) October 20, 2026 15:26
@JulieLeeMSFT

Copy link
Copy Markdown
Member Author

/ba-g known issues.

@JulieLeeMSFT
JulieLeeMSFT disabled auto-merge October 21, 2026 15:43
@JulieLeeMSFT
JulieLeeMSFT merged commit e107f9c into dotnet:main Oct 21, 2026
134 of 137 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Oct 22, 2026
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.

Test failure: JIT/opt/GuardedDevirtualization/badinlinee/badinlinee.cmd

3 participants