Skip to content

JIT: Always use CallInfo method handle for devirtualization - #123434

Merged
jakobbotsch merged 3 commits into
dotnet:mainfrom
hez2010:devirt-regression
Jan 22, 2026
Merged

JIT: Always use CallInfo method handle for devirtualization#123434
jakobbotsch merged 3 commits into
dotnet:mainfrom
hez2010:devirt-regression

Conversation

@hez2010

@hez2010hez2010 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

We already have the method handle in CallInfo, so we don't need to rely on the GenTree to extract the method handle for devirtualization.

Previously in #122023 we changed the JIT to only devirtualize a call when we can get the method handle from the GenTree, which was unnecessarily conservative especially when we already have the method handle in CallInfo.

Fixes#123391

CopilotAI review requested due to automatic review settings January 21, 2026 14:51
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jan 21, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jan 21, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

CopilotAI 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.

Pull request overview

This PR fixes a devirtualization regression introduced in #122023 by changing how method handles are obtained during late devirtualization. Instead of extracting the method handle from the GenTree (which could fail for certain indirect virtual calls), the PR stores the method handle in LateDevirtualizationInfo during import and retrieves it from there during late devirtualization.

Changes:

  • Added methodHnd field to LateDevirtualizationInfo structure to store the method handle
  • Simplified IsDevirtualizationCandidate to just check if a call is virtual or generic virtual, removing the method handle extraction logic
  • Updated late devirtualization to use the stored method handle instead of attempting to extract it from the tree

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/jit/inline.hAdded methodHnd field to LateDevirtualizationInfo struct to store the method handle for late devirtualization
src/coreclr/jit/importercalls.cppPopulated the new methodHnd field from callInfo->hMethod when creating late devirtualization info
src/coreclr/jit/gentree.hRemoved pMethHandle out parameter from IsDevirtualizationCandidate method signature
src/coreclr/jit/gentree.cppSimplified IsDevirtualizationCandidate to just return `IsVirtual()
src/coreclr/jit/fginline.cppChanged to retrieve method handle from gtLateDevirtualizationInfo->methodHnd instead of from out parameter, and removed now-unnecessary assertion

Comment threadsrc/coreclr/jit/fginline.cpp
@hez2010hez2010 changed the title JIT: Fix devirtualization regressionJIT: Always use CallInfo method handle for devirtualizationJan 21, 2026
@hez2010

This comment was marked as outdated.

@EgorBo

EgorBo commented Jan 21, 2026

Copy link
Copy Markdown
Member

BenchmarkRunner.Run();

@hez2010 this is not a correct way, either remove it, or replace with BenchmarkSwitcher.FromAssembly(typeof(ContainsTrue).Assembly).Run(args);

@hez2010

Copy link
Copy Markdown
ContributorAuthor

@EgorBot -amd -intel

usingSystem.Runtime.CompilerServices;usingSystem.Text;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;BenchmarkSwitcher.FromAssembly(typeof(ContainsTrue).Assembly).Run(args);publicclassContainsTrue{privatestring[]_found;privatestring[]_array;privateList<string>_list;[Params(512)]publicintSize;[GlobalSetup(Targets=new[]{nameof(Array)})]publicvoidSetupArray(){_found=ArrayOfUniqueValues(Size);_array=_found.ToArray();}[Benchmark]publicboolArray(){boolresult=default;string[]collection=_array;string[]found=_found;for(inti=0;i<found.Length;i++)result^=collection.Contains(found[i]);returnresult;}[GlobalSetup(Targets=new[]{nameof(ICollection)})]publicvoidSetupList(){_found=ArrayOfUniqueValues(Size);_list=newList<string>(_found);}[Benchmark]publicboolICollection()=>Contains(_list);[MethodImpl(MethodImplOptions.NoInlining)]privateboolContains(ICollection<string>collection){boolresult=default;string[]found=_found;for(inti=0;i<found.Length;i++)result^=collection.Contains(found[i]);returnresult;}privatestaticstring[]ArrayOfUniqueValues(intcount){string[]result=newstring[count];varrandom=newRandom(42);varuniqueValues=newHashSet<string>();while(uniqueValues.Count!=count){stringvalue=GenerateRandomString(random,1,50);if(!uniqueValues.Contains(value))uniqueValues.Add(value);}uniqueValues.CopyTo(result);returnresult;}privatestaticstringGenerateRandomString(Randomrandom,intminLength,intmaxLength){varlength=random.Next(minLength,maxLength);varbuilder=newStringBuilder(length);for(inti=0;i<length;i++){varrangeSelector=random.Next(0,3);if(rangeSelector==0)builder.Append((char)random.Next('a','z'));elseif(rangeSelector==1)builder.Append((char)random.Next('A','Z'));elsebuilder.Append((char)random.Next('0','9'));}returnbuilder.ToString();}}

@hez2010

hez2010 commented Jan 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Can confirm this fixes the regression seen in #123391

MethodToolchainSizeMeanErrorRatio
ArrayMain512299.7 μs2.99 μs1.00
ArrayPR512162.8 μs3.12 μs0.54
ICollectionMain512131.5 μs2.50 μs1.00
ICollectionPR512126.2 μs0.90 μs0.96

@hez2010

Copy link
Copy Markdown
ContributorAuthor

Test failures seem unrelated.

@jakobbotsch

Copy link
Copy Markdown
Member

Previously in #122023 we changed the JIT to only devirtualize a call when we can get the method handle from the GenTree

Wasn't this always the behavior? Before #122023 we used GetMethodHandle that also got it from the GenTree. So what exactly changed with #122023 that made this different?

@hez2010

hez2010 commented Jan 22, 2026

Copy link
Copy Markdown
ContributorAuthor

Previously in #122023 we changed the JIT to only devirtualize a call when we can get the method handle from the GenTree

Wasn't this always the behavior? Before #122023 we used GetMethodHandle that also got it from the GenTree. So what exactly changed with #122023 that made this different?

GetMethodHandle was only used for late devirtualization, in importer it only checked IsVirtual() || IsGenericVirtual() without extracting method handle from GenTree, and instead used the method handle from callInfo directly before #122023.

We have some virtual calls that are implemented as calli in the JIT despite they actually have a method handle. When importing those methods, we didn't actually use the CEE_CALLI code path but they were imported as calli in the end, which could lead the method handle to be thrown away after importer done. #122023 effectively blocked the devirtualization for them by accident, where we lost devirtualization opportunities for those methods in importer as well.

This PR unifies what we use for devirtualization, now we always have the method handle as long as the method is a virtual method.

@jakobbotsch

Copy link
Copy Markdown
Member

Hmm, ok.
It is unfortunate that we are ending up with a redundant set of fields in LateDevirtualizationInfo that must be kept up to date with the actual source of truth in gtCallAddr.

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

Thanks.

@jakobbotsch
jakobbotsch merged commit 66c2aa5 into dotnet:mainJan 22, 2026
125 of 127 checks passed
jakobbotsch added a commit that referenced this pull request Jan 28, 2026
…123664)
## Description
JIT hits assertion `'!"Unexpected well known arg to method GDV
candidate"'` when Guarded Devirtualization encounters methods with
`AsyncContinuation` arguments under tiered PGO with delegate/vtable
profiling enabled.
## Changes
Added `WellKnownArg::AsyncContinuation` to the signature compatibility
check in `isCompatibleMethodGDV`:
```cpp
case WellKnownArg::RetBuffer:
case WellKnownArg::ThisPointer:
case WellKnownArg::AsyncContinuation: // Added
// Not part of signature but we still expect to see it here
continue;
```
Like `RetBuffer` and `ThisPointer`, async continuations are
compiler-inserted arguments not reflected in the IL signature. The
compatibility check must skip them when matching call arguments against
the target method signature.
Consistent with existing handling in `fginline.cpp` and `importer.cpp`.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
> > ----
> > *This section details on the original issue you should resolve*
> > <issue_title>Assertion failed '!"Unexpected well known arg to method
GDV candidate"</issue_title>
>
<issue_description>https://dev.azure.com/dnceng-public/public/_build/results?buildId=1266373&view=ms.vss-test-web.build-test-results-tab&runId=35337768&resultId=209882&paneView=dotnet-dnceng.dnceng-build-release-tasks.helix-test-information-tab
> > windows x64 fullpgo method profiling
> > ```
> set DOTNET_TieredCompilation=1
> set DOTNET_ReadyToRun=0
> set DOTNET_TC_QuickJitForLoops=1
> set DOTNET_TieredPGO=1
> set DOTNET_JitDelegateProfiling=1
> set DOTNET_JitVTableProfiling=1
> > Discovering: System.Text.Json.Tests (method display = ClassAndMethod,
method display options = None)
> Discovered: System.Text.Json.Tests (found 10198 of 10271 test cases)
> Starting: System.Text.Json.Tests (parallel test collections = on [4
threads], stop on fail = off)
> > Assert failure(PID 44644 [0x0000ae64], Thread: 22492 [0x57dc]):
Assertion failed '!"Unexpected well known arg to method GDV candidate"'
in 'JsonSerializerApiValidation:<SerializeNullException>b__9_1():this'
during 'Importation' (IL size 25; hash 0xed53ad00; Tier1)
> > File: D:\a\_work\1\s\src\coreclr\jit\importercalls.cpp:7446
> Image: C:\h\w\AB6F09F9\p\dotnet.exe
> > ```</issue_description>
> > <agent_instructions>Add `WellKnownArg::AsyncContinuation` to the
switch in `isCompatibleMethodGDV`</agent_instructions>
> > ## Comments on the Issue (you are @copilot in this section)
> > <comments>
> <comment_new><author>@AndyAyersMS</author><body>
> Also several failures in
net11.0-windows-Release-x64-fullpgo_random_gdv_methodprofiling_only-Windows.10.Amd64.Open
> > ```
> DOTNET_JitClassProfiling=0
> DOTNET_JitDelegateProfiling=1
> DOTNET_JitRandomGuardedDevirtualization=1
> DOTNET_JitRandomlyCollect64BitCounts=1
> DOTNET_JitVTableProfiling=1
> DOTNET_ReadyToRun=0
> DOTNET_TC_QuickJitForLoops=1
> DOTNET_TieredCompilation=1
> DOTNET_TieredPGO=1
> > Discovering: System.IO.Compression.Brotli.Tests (method display =
ClassAndMethod, method display options = None)
> Discovered: System.IO.Compression.Brotli.Tests (found 109 of 120 test
cases)
> Starting: System.IO.Compression.Brotli.Tests (parallel test
collections = on [4 threads], stop on fail = off)
> > Assert failure(PID 16680 [0x00004128], Thread: 14112 [0x3720]):
Assertion failed '!"Unexpected well known arg to method GDV candidate"'
in
'System.IO.Compression.CompressionStreamUnitTestBase:RoundTripWithZLibCompressionOptions(System.String,System.IO.Compression.ZLibCompressionOptions):this'
during 'Importation' (IL size 176; hash 0x03f009b8; Tier1-OSR)
> > File: D:\a\_work\1\s\src\coreclr\jit\importercalls.cpp:7446
> ```
> > Maybe related to #123434? -- fyi @hez2010
</body></comment_new>
> </comments>
> </details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#123653
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jakobbotsch <7887810+jakobbotsch@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Perf] Windows/x64: 8 Regressions on 1/14/2026 1:50:43 AM +00:00

4 participants

@hez2010@EgorBo@jakobbotsch