Repository navigation
JIT: Devirtualize calls before probing them in optimized instrumented code - #136100
Merged
Merged
Conversation
… 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
|
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. |
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
EgorBo
approved these changes
Oct 19, 2026
Member
|
https://helixr1107v0xdcypoyl9e7f.blob.core.windows.net/dotnet-runtime-refs-pull-136100-merge-6e50f371f8b144cc96/System.Globalization.Nls.Tests/1/console.858e45f2.log?helixlogtype=result test failure might be related - |
Member
Author
The test failure looks like #136020. |
EgorBo
enabled auto-merge (squash)
October 20, 2026 15:26
Member
Author
|
/ba-g known issues. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #135557
JIT/opt/GuardedDevirtualization/badinlineefails in the rollingruntime-coreclr jitstresspipeline (instrument_if_optimizing,jitstress1,jitstress1_tiered,jitstress_random_*,jitstress_isas_1_*) withTest 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)impImportCallcalledimpConsiderCallProbebefore trying to devirtualize and skippedimpDevirtualizeCallfor 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 spillsthisinto a temp without class information, so late devirtualization fails (no type available (op=LCL_VAR)), the inliner reportstarget not direct, and innew Base().M()the allocation stays because the object is also passed to theCORINFO_HELP_CLASSPROFILE32helper. 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), soconsiderGuardedDevirtualizationnow bails out for such calls, which keeps the previous behavior there.impConsiderCallProbeno 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) tomov eax, 42; ret.2.
badinlineecannot hold under JitStressJitStress picks methods by hash: at
DOTNET_JitStress=1it selectsTestforSTRESS_MIN_OPTS(and for the forcedNO_WAYfallback ofSTRESS_GENERIC_VARN, which also retries the compile with MinOpts), so the final tier is MinOpts, nothing is inlined and the allocation stays. Disabling onlySTRESS_MIN_OPTSis not enough, so the test now uses[SkipOnCoreClr(..., RuntimeTestModes.JitStress)]like other tests that check optimization results under JitStress (it needs aTestLibraryreference for that).3.
badinlineedepended on wall-clock timingThe old warm-up was a fixed 100 x (100 calls +
Sleep(15)) per method. WithTC_CallCountThreshold=1the tier-up queue holds hundreds of CoreLib methods, soPoison(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 againstcall->gtCallMethHnd) still passed the old test 3/3. It also failed withDOTNET_ForceRelocs=1(the JIT does twice the work) becauseTestwas still in tier 0. The test now keeps calling the method untilJitInfo.GetCompiledMethodCount()has been unchanged for 20 iterations, first forPoison, then forTest, and asserts withAssert.Equalinstead 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_135557is a minimal repro of (1) that does not depend on the GDV scenario:new Base().M()withDOTNET_JitInstrumentIfOptimizing=1(Checked/Debug JIT only, ignored by a Release JIT) must not allocate onceTestreaches its final tier. 10/10 runs fail with the old JIT (Expected: 0 Actual: 24000) and 10/10 pass with the fix.Testing
badinlineeandRuntime_135557on windows-x64 Checked. I ranbadinlineein 49 scenarios oftestenvironment.projwith the old and the new JIT. Old JIT with the old test: failsforcerelocs,jitstress1,jitstress1_tiered,jitstress_isas_1_*andinstrument_if_optimizing. Old JIT with the new test: onlyinstrument_if_optimizingfails. New JIT with the new test: everything passes.jitpartialcompilation_pgo_stress_randomis excluded from these statements: it crashes (exit code0x80000003) identically with the old JIT and the old test and is unrelated (other tests pass in that scenario).src/tests/JIT(priority 1, 22,288 tests, including the two changed ones) with the new JIT in the default configuration and in theinstrument_if_optimizingandfullpgo_methodprofiling_always_optimizedscenarios, 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.coreclr_tests.runreports missing data for 8179 of 620965 contexts, allResolveVirtualMethod, i.e. the new JIT-EE query in optimized+instrumented compiles (the old JIT replays this collection clean). Asm diffs against the base JIT onbenchmarks.run,benchmarks.run_pgo,benchmarks.run_pgo_optrepeat,coreclr_tests.run,libraries.crossgen2,libraries.pmiandrealworld.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 withTieredPGOandTieredPGO_InstrumentedTierAlwaysOptimized(5,153 of themInstrumented 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.pyreports no formatting changes.Note
This pull request description was generated with GitHub Copilot.