Skip to content

Support devirtualization for virtual methods that require runtime lookups - #129806

Open
hez2010 wants to merge 23 commits into
dotnet:mainfrom
hez2010:devirt-runtime-lookups
Open

Support devirtualization for virtual methods that require runtime lookups#129806
hez2010 wants to merge 23 commits into
dotnet:mainfrom
hez2010:devirt-runtime-lookups

Conversation

@hez2010

@hez2010hez2010 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Add support for devirtualizing virtual methods that require a runtime lookup. In this case we can't avoid the expensive runtime lookup, but at least the callee now can be devirtualized and inlined, so that they can benefit from the downstream optimizations like escape analysis, struct promotion and etc.

When we see an inexact instantiating stub on an exact class, we compute a runtime lookup against the shared generic token of the virtual method.

publicinterfaceIProcessor{object?Process<T>(Tvalue);}publicclassPrintProcessor:IProcessor{publicobject?Process<T>(Tvalue){returnvalue;}}publicstaticclassProgram{publicstaticvoidMain(){Console.WriteLine(Call("hello"));}[MethodImpl(MethodImplOptions.NoInlining)]staticobject?Call<T>(Tvalue)whereT:class{IProcessorprocessor=newPrintProcessor();returnprocessor.Process(value);}}

Codegen for Call:

Before:

G_M47477_IG01: ;; offset=0x0000pushrdipushrsipushrbxsubrsp,48mov qword ptr [rsp+0x28],rcxmovrbx,rcxmovrsi,rdx ;; size=18 bbWeight=1 PerfScore 4.75G_M47477_IG02: ;; offset=0x0012movrcx,0x7FF9258BB130 ; PrintProcessorcall CORINFO_HELP_NEWSFASTmovrdi,raxmovrcx, qword ptr [rbx+0x48]movr8, qword ptr [rcx+0x10]testr8,r8je SHORT G_M47477_IG05 ;; size=31 bbWeight=1 PerfScore 6.75G_M47477_IG03: ;; offset=0x0031movrcx,rdimovrdx,0x7FF9258BAF08 ; IProcessorcall CORINFO_HELP_VIRTUAL_FUNC_PTRmovrcx,rdimovrdx,rsicallraxnop ;; size=27 bbWeight=1 PerfScore 5.25G_M47477_IG04: ;; offset=0x004Caddrsp,48poprbxpoprsipoprdiret ;; size=8 bbWeight=1 PerfScore 2.75G_M47477_IG05: ;; offset=0x0054movrcx,rbxmovrdx,0x7FF9258E0DE8 ; global ptrcall CORINFO_HELP_RUNTIMEHANDLE_METHODmovr8,raxjmp SHORT G_M47477_IG03 ;; size=23 bbWeight=0.20 PerfScore 0.75

After:

G_M47477_IG01: ;; offset=0x0000subrsp,40mov qword ptr [rsp+0x20],rcx ;; size=9 bbWeight=1 PerfScore 1.25G_M47477_IG02: ;; offset=0x0009movrax, qword ptr [rcx+0x48]cmp qword ptr [rax+0x08],24jle SHORT G_M47477_IG03 ;; size=11 bbWeight=1 PerfScore 5.00G_M47477_IG03: ;; offset=0x0014movrax,rdx ;; size=3 bbWeight=1 PerfScore 0.25G_M47477_IG04: ;; offset=0x0017addrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25

Contributes to #112596

CopilotAI lite review requested due to automatic review settings June 24, 2026 16:44

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added the area-crossgen2-coreclr only use for closed issues label Jun 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Jun 24, 2026
@hez2010

Copy link
Copy Markdown
ContributorAuthor

@MihuBot -nuget

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hez2010

Copy link
Copy Markdown
ContributorAuthor

@MihuBot -nuget -dependsOn 128702

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings July 1, 2026 11:33

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

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

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 1, 2026 11:42
CORINFO_METHOD_HANDLE methodHnd;
CORINFO_CONTEXT_HANDLE exactContextHnd;
ILLocation ilLocation;
CORINFO_RESOLVED_TOKEN resolvedToken;

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.

Resolved tokens are large structures. Does this show up on memory use?

@hez2010hez2010Jul 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't measure it yet, but it is necessary for computing the runtime lookup. I can't think of a better way than disabling late devirtualization for this if we don't want to store a resolved token.

Comment on lines +9323 to +9354
// If we don't have the right context, try recover it from the tree.
//
if (!isCurrentContext)
{
CallArg* runtimeMethodHandleArg = nullptr;
if ((call->gtControlExpr != nullptr) && call->gtControlExpr->OperIs(GT_CALL))
{
runtimeMethodHandleArg =
call->gtControlExpr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle);
}

if (runtimeMethodHandleArg != nullptr && runtimeMethodHandleArg->GetNode()->OperIs(GT_RUNTIMELOOKUP))
{
if (runtimeMethodHandleArg->GetNode()->AsRuntimeLookup()->Lookup()->OperIs(GT_CALL))
{
GenTreeCall* const helperCall =
runtimeMethodHandleArg->GetNode()->AsRuntimeLookup()->Lookup()->AsCall();

if (helperCall->IsHelperCall(CORINFO_HELP_RUNTIMEHANDLE_METHOD) ||
helperCall->IsHelperCall(CORINFO_HELP_RUNTIMEHANDLE_CLASS))
{
runtimeLookupContext = helperCall->gtArgs.GetArgByIndex(0)->GetNode();
}
}
}

if (runtimeLookupContext == nullptr)
{
JITDUMP("Late devirt needs a runtime lookup context cannot be figured out. Bail out.\n");
return;
}
}

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.

Why can this not be done where it is needed?

Trying to smuggle IR through across phases without actually adding it does not scale well. I do not think this is a workable way of doing this.

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.

Ah, dcInfo is not the info we save. Ok, then it looks better.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to smuggle IR through across phases without actually adding it does not scale well.

Alternatively we will need to add something to GenTreeCall to carry the original lookup context tree.

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "a9780b4c86ad8a2f839ef4abad70f63e3737b6e3",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "de625a927aa4e6f70cda5eaf247cf54c225d158a",
"last_reviewed_commit": "a9780b4c86ad8a2f839ef4abad70f63e3737b6e3",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "de625a927aa4e6f70cda5eaf247cf54c225d158a",
"last_recorded_worker_run_id": "29681945714",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "a9780b4c86ad8a2f839ef4abad70f63e3737b6e3",
"review_id": 4730548930
}
]
}

@github-actionsgithub-actionsBot 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.

Holistic Review

Motivation: The problem is real and worthwhile. Today the JIT bails out of devirtualizing a shared generic virtual method whose instantiation argument requires a runtime lookup (CORINFO_DEVIRTUALIZATION_FAILED_CANON). This PR lets such calls devirtualize (still paying the runtime lookup) so the callee can be inlined and benefit from escape analysis, struct promotion, etc. The demonstrated codegen improvement is significant and it contributes to #112596.

Approach: Reasonable and consistent with existing infrastructure. It adds a new DevirtualizedMethodDescSlot dictionary entry kind wired through the VM, the R2R/NativeAOT ComputeRuntimeLookupForSharedGenericToken, and SuperPMI, threads a callerMethod input into resolveVirtualMethod, and propagates the runtime-lookup context tree from dcInfo into getLookupTree/getRuntimeLookupTree. The JIT-EE GUID is correctly bumped. NativeAOT is intentionally left as a TODO. The trickiest part is late devirtualization recovering the correct generic context from the existing call IR; the author added a LateDevirtualizationInfo.resolvedToken and targeted tests for the immediate/late/late-root context cases.

Summary: ⚠️ Needs Human Review. The change is well-constructed and the new-success paths look internally consistent (all three CORINFO_DEVIRTUALIZATION_INFO construction sites now initialize callerMethod, and the shared-MT vs shared-method cases bail vs. runtime-lookup as intended). However, two areas warrant maintainer judgment: (1) the impRuntimeLookupToTree behavioral change is broader than the devirt feature and touches all optimized testForNull runtime lookups (see inline); and (2) jakobbotsch already raised concerns about smuggling the lookup-context IR across phases via dcInfo and about the memory cost of storing a full CORINFO_RESOLVED_TOKEN in LateDevirtualizationInfo. These are design/CQ tradeoffs best confirmed by the JIT area owners plus SPMI diffs, not blockers I can resolve from the diff alone.


Detailed Findings

⚠️ Broadened behavior in impRuntimeLookupToTree — un-spilled helper call in optimized code

The opts.OptimizationEnabled() branch now returns the raw helper GT_CALL instead of always spilling to a temp. This applies to every testForNull runtime lookup in optimized code, well beyond the devirtualization scenario the PR targets. Flagged inline on src/coreclr/jit/importer.cpp; please validate with a full SPMI/CQ run that no unrelated regressions or duplicated side-effecting calls result, and consider scoping it.

callerMethod initialization is complete

The new required CORINFO_DEVIRTUALIZATION_INFO.callerMethod input is set at all three JIT construction sites (importercalls.cpp lines 8097, 8183, 9296) from call->gtInlineContext->GetCallee(). An earlier Copilot comment worried about uninitialized garbage in the GDV exact-classes loop; that concern does not apply to the current head — line 8097 initializes it. The VM/R2R runtime-lookup paths that dereference callerMethod are only reached after pResolvedTokenVirtualMethod != nullptr, and in the JIT that token is only non-null on the direct-devirt path (GDV likely/exact still pass nullptr), so GetMethod/HandleToObject on callerMethod is not exercised with an unset handle today. Worth a maintainer double-check that no external JIT-EE consumer leaves callerMethod unset.

✅ Shared-MT vs shared-method distinction

The VM (resolveVirtualMethodHelper) and managed (CorInfoImpl.cs) paths correctly bail with FAILED_CANON when a shared MethodTable instantiation is required (cannot be recovered even as a runtime lookup) and only take the new DevirtualizedMethodDescSlot runtime-lookup path when the method instantiation is canonical. The isUnboxingStubOfInstantiatingStub case was split accordingly, and the unboxed-entry lookup now unwraps an instantiating stub (jitinterface.cpp 8979).

✅ Late-devirtualization context recovery + tests

The RuntimeLookupInlining test exercises immediate inlining, late devirtualization of an inlinee call (context mismatch requiring recovery of the runtime-lookup context from the RuntimeMethodHandle arg), and the late-root case. The bail-out when the context tree cannot be recovered (impDevirtualizeCall) is a safe conservative fallback. This addresses the JIT guidance's expectation of targeted tests for an observable behavior change.

💡 Style nit in jitinterface.cpp

The new single-line braceless if at line 8979 is inconsistent with the file's bracing style; flagged inline (non-blocking).

💡 Open design questions from maintainer review

jakobbotsch questioned passing the lookup context IR through dcInfo across phases and the memory footprint of embedding a full CORINFO_RESOLVED_TOKEN in LateDevirtualizationInfo (resolved tokens are large). The author acknowledged both and noted the alternative would be adding a field to GenTreeCall. These remain unresolved design tradeoffs for the area owners to sign off on.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 190.1 AIC · ⌖ 11.5 AIC · ⊞ 10K

Comment threadsrc/coreclr/jit/importer.cpp
Comment threadsrc/coreclr/vm/jitinterface.cpp
CopilotAI review requested due to automatic review settings August 13, 2026 08:29

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 15, 2026 13:38

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 19, 2026 14:55

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 26, 2026 16:19

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 27, 2026 01:54

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

CopilotAI review requested due to automatic review settings August 27, 2026 09:35

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

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.

3 participants

@hez2010@jakobbotsch