Skip to content

Conversation

@hez2010
Copy link
Contributor

@hez2010 hez2010 commented Nov 27, 2025

Enable devirtualization support for generic virtual methods.

When we see a base method having a method instantiation, we use FindOrCreateAssociatedMethodDesc to obtain the devirted method.

Also introduced a jit knob so that it can be turned off at any time.

AOT support is not included in this PR, which needs additional work in managed type system.

Also, if we end up with an instantiating stub (i.e. a shared generic method that requires runtime lookup), we don't have the correct generic context so we need to bail out for now.

Codegen example:

public class IntProcessor : VirtualGenericClass, IVritualGenericInterface
{
    public override void Process<T>(T item)
    {
        Console.WriteLine(item.ToString());
    }
}

public interface IVritualGenericInterface
{
    void Process<T>(T item) where T : notnull;
}

public static void Test<T>(IVritualGenericInterface ifce, T item) where T : notnull
{
    ifce.Process(item);
}

public static void Test<T>(VirtualGenericClass baseClass, T item) where T : notnull
{
    baseClass.Process(item);
}

static void Test()
{
    IVritualGenericInterface i = new IntProcessor();
    Test(i, 42);

    VirtualGenericClass c = new IntProcessor();
    Test(c, 42);
}

Codegen diff:

 G_M27646_IG01:  ;; offset=0x0000
-       push     rsi
        push     rbx
-       sub      rsp, 40
+       sub      rsp, 32
-                                                ;; size=6 bbWeight=1 PerfScore 2.25
+                                                ;; size=5 bbWeight=1 PerfScore 1.25
-G_M27646_IG02:  ;; offset=0x0006
+G_M27646_IG02:  ;; offset=0x0005
-       mov      rbx, 0x7FFE0803F318      ; Program+IntProcessor
+       mov      rbx, 0x221CA429C38      ; 'System.Int32'
        mov      rcx, rbx
-       call     CORINFO_HELP_NEWSFAST
+       call     [System.Console:WriteLine(System.Object)]
-       mov      rsi, rax
+       mov      ecx, 42
-       mov      rcx, rsi
+       call     [System.Number:Int32ToDecStr(int):System.String]
-       mov      rdx, 0x7FFE0803F100      ; Program+IVritualGenericInterface
+       mov      rcx, rax
-       mov      r8, 0x7FFE0803F6A8      ; token handle
+       call     [System.Console:WriteLine(System.String)]
-       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
-       mov      rcx, rsi
-       mov      edx, 42
-       call     rax
        mov      rcx, rbx
-       call     CORINFO_HELP_NEWSFAST
+       call     [System.Console:WriteLine(System.Object)]
-       mov      rbx, rax
+       mov      ecx, 42
-       mov      rcx, rbx
+       call     [System.Number:Int32ToDecStr(int):System.String]
-       mov      rdx, 0x7FFE0803EF30      ; Program+VirtualGenericClass
+       mov      rcx, rax
-       mov      r8, 0x7FFE0803F8A8      ; token handle
+       call     [System.Console:WriteLine(System.String)]
-       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
-       mov      rcx, rbx
-       mov      edx, 42
-       call     rax
        nop
-                                                ;; size=109 bbWeight=1 PerfScore 14.00
+                                                ;; size=69 bbWeight=1 PerfScore 20.00
-G_M27646_IG03:  ;; offset=0x0073
+G_M27646_IG03:  ;; offset=0x004A
-       add      rsp, 40
+       add      rsp, 32
        pop      rbx
-       pop      rsi
        ret
-                                                ;; size=7 bbWeight=1 PerfScore 2.25
+                                                ;; size=6 bbWeight=1 PerfScore 1.75

Contributes to #112596

cc: @dotnet/jit-contrib

Copilot AI review requested due to automatic review settings November 27, 2025 17:12
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Nov 27, 2025
@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 Nov 27, 2025
Copy link
Contributor

Copilot AI left a comment

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 enables devirtualization support for generic virtual methods in the JIT compiler, allowing calls to generic virtual methods to be devirtualized when the exact type is known at JIT time. This optimization eliminates virtual dispatch overhead and enables further optimizations like inlining.

Key changes:

  • Removes the assertion that previously blocked generic method devirtualization
  • Generalizes array interface devirtualization to support generic virtual methods by renaming wasArrayInterfaceDevirt to needsMethodContext
  • Adds runtime lookup support for generic method instantiation parameters

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/coreclr/vm/jitinterface.cpp Removes assertion blocking generic method devirtualization and adds logic to handle generic virtual methods using FindOrCreateAssociatedMethodDesc
src/coreclr/inc/corinfo.h Renames wasArrayInterfaceDevirt to needsMethodContext to generalize the field meaning
src/coreclr/jit/jitconfigvalues.h Adds JitEnableGenericVirtualDevirtualization configuration flag to control the feature
src/coreclr/jit/gentree.h Adds IsGenericVirtual() helper method to identify generic virtual method calls
src/coreclr/jit/gentree.cpp Updates IsDevirtualizationCandidate() to include generic virtual methods (non-AOT only)
src/coreclr/jit/importercalls.cpp Implements devirtualization logic for generic virtual methods including runtime lookup handling, updates comments and variable names, and introduces DEVIRT label for control flow
src/coreclr/jit/inline.h Renames arrayInterface field to needsMethodContext in InlineCandidateInfo struct
src/coreclr/jit/indirectcalltransformer.cpp Updates to use renamed needsMethodContext field
src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs Updates managed struct definition to match renamed field
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Updates to use renamed field in managed implementation
src/coreclr/tools/superpmi/superpmi-shared/agnostic.h Updates SuperPMI data structure with renamed field
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp Updates SuperPMI recording/replay to use renamed field

@hez2010
Copy link
Contributor Author

hez2010 commented Nov 28, 2025

@MihuBot

Comment on lines 8955 to 8962
// If we have a RUNTIMELOOKUP helper call for the method handle,
// we need to pass that as the inst param instead.
CallArg* const methHndArg = call->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle);
GenTree* const methHndNode = methHndArg != nullptr ? methHndArg->GetEarlyNode() : nullptr;
if (methHndNode && methHndNode->OperIs(GT_RUNTIMELOOKUP))
{
instParam = methHndNode;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain this behavior?

It mixes syntax and semantics. Nothing guarantees that a runtime lookup appears as a GT_RUNTIMELOOKUP node. It could have been stored to a local and a number of other things.

This comment was marked as outdated.

This comment was marked as outdated.

Copy link
Contributor Author

@hez2010 hez2010 Nov 30, 2025

Choose a reason for hiding this comment

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

Pushed a new commit including comments to clarify this behavior. Also updated the description of the PR.

Comment on lines 8854 to 8858
if (isGenericVirtual)
{
JITDUMP("Shared generic virtual devirt not supported yet\n");
return;
}
Copy link
Member

@jakobbotsch jakobbotsch Dec 10, 2025

Choose a reason for hiding this comment

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

The layering feels wrong to me. How can the VM side return an instantiating stub if the base method requires a runtime lookup? There is no way to create an instantiating stub at devirtualization time in that scenario.

I think it would make more sense if resolveVirtualMethod never returns an instantiation stub for GVMs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved it to VM so now the VM should never return an instantiating stub.

Comment on lines 8788 to 8794
pDevirtMD = pDevirtMD->FindOrCreateAssociatedMethodDesc(
pDevirtMD, pExactMT, pExactMT->IsValueType() && !pDevirtMD->IsStatic(), pBaseMD->GetMethodInstantiation(), false);

// We still can't handle shared generic methods because we don't have
// the right generic context for runtime lookup.
// TODO: Remove this limitation.
if (pDevirtMD->IsInstantiatingStub())
Copy link
Member

@jakobbotsch jakobbotsch Dec 11, 2025

Choose a reason for hiding this comment

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

If I understand correctly this will sometimes create odd instantiating stubs. For example, it may create an instantiating stub that instantiates over System.__Canon if the JIT got the method desc from a runtime lookup.

I do not think we need to create any instantiation stubs at all here. Should we instead pass allowInstParam = true to FindOrCreateAssociatedMethodDesc and then change this check to check if the returned function is shared?

Copy link
Member

@jakobbotsch jakobbotsch Dec 11, 2025

Choose a reason for hiding this comment

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

Also since the declaration takes BOOL I think it should be TRUE/FALSE instead.

Copy link
Contributor Author

@hez2010 hez2010 Dec 11, 2025

Choose a reason for hiding this comment

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

Yeah, since we are now rejecting any shared generic methods, I think it's okay to pass allowInstParam = true.
But if we want to extend it to support shared generic methods as well, we will need to revisit it to allow it creating an instantiating stub again, and bail if it returns an instantiating stub over System.__Canon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved in babad30

Copy link
Member

@jakobbotsch jakobbotsch Dec 12, 2025

Choose a reason for hiding this comment

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

Yeah, since we are now rejecting any shared generic methods, I think it's okay to pass allowInstParam = true.
But if we want to extend it to support shared generic methods as well, we will need to revisit it to allow it creating an instantiating stub again, and bail if it returns an instantiating stub over System.__Canon.

I do not think that we should need to create instantiation stubs and then have the JIT remove them again. Instead we should make CORINFO_DEVIRTUALIZATION_INFO return the info necessary to let the JIT properly compute the generic context to pass. If the base method is a runtime lookup this will be some modified version of that runtime lookup. If the base method's instantiation is statically known then it will be some constant generic context (likely exactContext that already exists).

In either case I do not think we need to create an instantiating stub to then remove it again (and in the former case we can't even do that).

@hez2010
Copy link
Contributor Author

hez2010 commented Dec 11, 2025

Do you have plans to work on that part in a later PR?

@MichalStrehovsky FYI I have a local branch that brings the non-shared GVM devirt support to R2R. NAOT still needs extra work in the JIT before it can be handled. I'll open another PR after this one merged, so that at least the changes to the managed type system can be reviewed first.

Copy link
Member

@jakobbotsch jakobbotsch left a comment

Choose a reason for hiding this comment

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

LGTM apart from that. Thanks!
@davidwrighton can you take a look too?

@davidwrighton
Copy link
Member

@hez2010 sorry about the delay, I got stuck in a bunch of work just before the holidays and wasn't able to do the review before vacation began.

@jakobbotsch
Copy link
Member

I'm guessing the crossgen issues are related, I don't see them on other PRs. Can you see if you can reproduce them?

@hez2010
Copy link
Contributor Author

hez2010 commented Jan 8, 2026

I'm guessing the crossgen issues are related, I don't see them on other PRs. Can you see if you can reproduce them?

I managed to reproduce the freebsd one on my local cross-build environment. Will investigate it soon to see whether it's related or not.

@hez2010
Copy link
Contributor Author

hez2010 commented Jan 9, 2026

We hit the assertion here:

/home/i/runtime/src/coreclr/jit/importercalls.cpp:8666
Assertion failed 'call->IsVirtualStub()' in 'System.Threading.StackHelper:CallOnEmptyStack[System.__Canon,System.__Canon,System.__Canon,System.__Canon](System.Func`4[System.__Canon,System.__Canon,System.__Canon,System.__Canon],System.__Canon,System.__Canon,System.__Canon):System.__Canon' during 'Importation' (IL size 116; hash 0x7bf56612; FullOpts)

/home/i/runtime/src/coreclr/jit/importercalls.cpp:8666
Assertion failed 'call->IsVirtualStub()' in 'System.Threading.StackHelper:CallOnEmptyStack[System.__Canon,System.__Canon](System.Func`2[System.__Canon,System.__Canon],System.__Canon):System.__Canon' during 'Importation' (IL size 102; hash 0x657768d4; FullOpts)

https://github.com/hez2010/runtime/blob/40d7569852bd8cecef706532d374ab0e8f7e53f2/src/coreclr/jit/importercalls.cpp#L8664-L8670

I think GVM applies the same with virtual stub, where in R2R mode, we might see GVM calls to non-virtuals. I think we should change the assertion to call->IsVirtualStub() || call->IsGenericVirtual(). What do you think? @jakobbotsch

@jakobbotsch
Copy link
Member

I think GVM applies the same with virtual stub, where in R2R mode, we might see GVM calls to non-virtuals. I think we should change the assertion to call->IsVirtualStub() || call->IsGenericVirtual(). What do you think? @jakobbotsch

Seems reasonable to me.

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 community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants