From fbff8d6071b21493be4bae9905438c95117e6874 Mon Sep 17 00:00:00 2001 From: Steven He Date: Thu, 27 Nov 2025 23:34:25 +0900 Subject: [PATCH 01/22] Rename wasArrayInterfaceDevirt to needsMethodContext --- src/coreclr/inc/corinfo.h | 4 +-- src/coreclr/jit/importercalls.cpp | 26 +++++++++---------- .../tools/Common/JitInterface/CorInfoImpl.cs | 2 +- .../tools/Common/JitInterface/CorInfoTypes.cs | 6 ++--- .../tools/superpmi/superpmi-shared/agnostic.h | 2 +- .../superpmi-shared/methodcontext.cpp | 8 +++--- src/coreclr/vm/jitinterface.cpp | 6 ++--- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 7cd408af5905bb..226cf8da1fe120 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1578,7 +1578,7 @@ struct CORINFO_DEVIRTUALIZATION_INFO // - If pResolvedTokenDevirtualizedMethod is not set to NULL and targeting an R2R image // use it as the parameter to getCallInfo // - isInstantiatingStub is set to TRUE if the devirtualized method is a generic method instantiating stub - // - wasArrayInterfaceDevirt is set TRUE for array interface method devirtualization + // - needsMethodContext is set TRUE if the devirtualized method requires a method context // (in which case the method handle and context will be a generic method) // CORINFO_METHOD_HANDLE devirtualizedMethod; @@ -1587,7 +1587,7 @@ struct CORINFO_DEVIRTUALIZATION_INFO CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedMethod; CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedUnboxedMethod; bool isInstantiatingStub; - bool wasArrayInterfaceDevirt; + bool needsMethodContext; }; //---------------------------------------------------------------------------- diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index f9d0fddb50af9b..b12258492ea612 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7603,7 +7603,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, } addGuardedDevirtualizationCandidate(call, exactMethod, exactCls, exactContext, exactMethodAttrs, - clsAttrs, likelyHood, dvInfo.wasArrayInterfaceDevirt, + clsAttrs, likelyHood, dvInfo.needsMethodContext, dvInfo.isInstantiatingStub, baseMethod, originalContext); } @@ -7676,7 +7676,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, likelyContext = dvInfo.exactContext; likelyMethod = dvInfo.devirtualizedMethod; - arrayInterface = dvInfo.wasArrayInterfaceDevirt; + arrayInterface = dvInfo.needsMethodContext; instantiatingStub = dvInfo.isInstantiatingStub; } else @@ -8767,14 +8767,14 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_CLASS) { - assert(!dvInfo.wasArrayInterfaceDevirt); + assert(!dvInfo.needsMethodContext); derivedClass = (CORINFO_CLASS_HANDLE)((size_t)exactContext & ~CORINFO_CONTEXTFLAGS_MASK); } else { // Array interface devirt can return a nonvirtual generic method of the non-generic SZArrayHelper class. // - assert(dvInfo.wasArrayInterfaceDevirt); + assert(dvInfo.needsMethodContext); assert(((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD); derivedClass = info.compCompHnd->getMethodClass(derivedMethod); } @@ -8797,7 +8797,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, { // We should only end up with generic methods that needs a method context (eg. array interface). // - assert(dvInfo.wasArrayInterfaceDevirt); + assert(dvInfo.needsMethodContext); // We don't expect NAOT to end up here, since it has Array // and normal devirtualization. @@ -8920,14 +8920,6 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, JITDUMP(" %s; can devirtualize\n", note); - // Make the updates. - call->gtFlags &= ~GTF_CALL_VIRT_VTABLE; - call->gtFlags &= ~GTF_CALL_VIRT_STUB; - call->gtCallMethHnd = derivedMethod; - call->gtCallType = CT_USER_FUNC; - call->gtControlExpr = nullptr; - INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_DEVIRTUALIZED); - if (dvInfo.isInstantiatingStub) { // Pass the instantiating stub method desc as the inst param arg. @@ -8939,6 +8931,14 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, call->gtArgs.InsertInstParam(this, instParam); } + // Make the updates. + call->gtFlags &= ~GTF_CALL_VIRT_VTABLE; + call->gtFlags &= ~GTF_CALL_VIRT_STUB; + call->gtCallMethHnd = derivedMethod; + call->gtCallType = CT_USER_FUNC; + call->gtControlExpr = nullptr; + INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_DEVIRTUALIZED); + // Virtual calls include an implicit null check, which we may // now need to make explicit. if (!objIsNonNull) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 5ca092d1e5eae0..0edb3ec0a4b88a 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1324,7 +1324,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info) info->exactContext = null; info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_UNKNOWN; info->isInstantiatingStub = false; - info->wasArrayInterfaceDevirt = false; + info->needsMethodContext = false; TypeDesc objType = HandleToObject(info->objClass); diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index db2153d1b35d14..e6d68d07dc0115 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -1156,7 +1156,7 @@ public unsafe struct CORINFO_DEVIRTUALIZATION_INFO // - exactContext is set to wrapped CORINFO_CLASS_HANDLE of devirt'ed method table. // - detail describes the computation done by the jit host // - isInstantiatingStub is set to TRUE if the devirtualized method is a method instantiation stub - // - wasArrayInterfaceDevirt is set TRUE for array interface method devirtualization + // - needsMethodContext is set TRUE if the devirtualized method requires a method context // (in which case the method handle and context will be a generic method) // public CORINFO_METHOD_STRUCT_* devirtualizedMethod; @@ -1166,8 +1166,8 @@ public unsafe struct CORINFO_DEVIRTUALIZATION_INFO public CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedUnboxedMethod; public byte _isInstantiatingStub; public bool isInstantiatingStub { get { return _isInstantiatingStub != 0; } set { _isInstantiatingStub = value ? (byte)1 : (byte)0; } } - public byte _wasArrayInterfaceDevirt; - public bool wasArrayInterfaceDevirt { get { return _wasArrayInterfaceDevirt != 0; } set { _wasArrayInterfaceDevirt = value ? (byte)1 : (byte)0; } } + public byte _needsMethodContext; + public bool needsMethodContext { get { return _needsMethodContext != 0; } set { _needsMethodContext = value ? (byte)1 : (byte)0; } } } //---------------------------------------------------------------------------- diff --git a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h index 955f1253ee504c..4dd74b117a25d5 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h @@ -681,7 +681,7 @@ struct Agnostic_ResolveVirtualMethodResult bool returnValue; DWORDLONG devirtualizedMethod; bool isInstantiatingStub; - bool wasArrayInterfaceDevirt; + bool needsMethodContext; DWORDLONG exactContext; DWORD detail; Agnostic_CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedMethod; diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp index 9dbb8b8d6a40ff..b7c59485f2af2e 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp @@ -3276,7 +3276,7 @@ void MethodContext::recResolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO * info result.isInstantiatingStub = info->isInstantiatingStub; result.exactContext = CastHandle(info->exactContext); result.detail = (DWORD) info->detail; - result.wasArrayInterfaceDevirt = info->wasArrayInterfaceDevirt; + result.needsMethodContext = info->needsMethodContext; if (returnValue) { @@ -3301,11 +3301,11 @@ void MethodContext::dmpResolveVirtualMethod(const Agnostic_ResolveVirtualMethodK key.context, key.pResolvedTokenVirtualMethodNonNull, key.pResolvedTokenVirtualMethodNonNull ? SpmiDumpHelper::DumpAgnostic_CORINFO_RESOLVED_TOKEN(key.pResolvedTokenVirtualMethod).c_str() : "???"); - printf(", value returnValue-%s, devirtMethod-%016" PRIX64 ", instantiatingStub-%s, wasArrayInterfaceDevirt-%s, exactContext-%016" PRIX64 ", detail-%d, tokDvMeth{%s}, tokDvUnboxMeth{%s}", + printf(", value returnValue-%s, devirtMethod-%016" PRIX64 ", instantiatingStub-%s, needsMethodContext-%s, exactContext-%016" PRIX64 ", detail-%d, tokDvMeth{%s}, tokDvUnboxMeth{%s}", result.returnValue ? "true" : "false", result.devirtualizedMethod, result.isInstantiatingStub ? "true" : "false", - result.wasArrayInterfaceDevirt ? "true" : "false", + result.needsMethodContext ? "true" : "false", result.exactContext, result.detail, result.returnValue ? SpmiDumpHelper::DumpAgnostic_CORINFO_RESOLVED_TOKEN(result.resolvedTokenDevirtualizedMethod).c_str() : "???", @@ -3330,7 +3330,7 @@ bool MethodContext::repResolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO * info info->devirtualizedMethod = (CORINFO_METHOD_HANDLE) result.devirtualizedMethod; info->isInstantiatingStub = result.isInstantiatingStub; - info->wasArrayInterfaceDevirt = result.wasArrayInterfaceDevirt; + info->needsMethodContext = result.needsMethodContext; info->exactContext = (CORINFO_CONTEXT_HANDLE) result.exactContext; info->detail = (CORINFO_DEVIRTUALIZATION_DETAIL) result.detail; if (result.returnValue) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 76bf7390d5848a..47cd461cc0373b 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8568,7 +8568,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) memset(&info->resolvedTokenDevirtualizedMethod, 0, sizeof(info->resolvedTokenDevirtualizedMethod)); memset(&info->resolvedTokenDevirtualizedUnboxedMethod, 0, sizeof(info->resolvedTokenDevirtualizedUnboxedMethod)); info->isInstantiatingStub = false; - info->wasArrayInterfaceDevirt = false; + info->needsMethodContext = false; MethodDesc* pBaseMD = GetMethod(info->virtualMethod); MethodTable* pBaseMT = pBaseMD->GetMethodTable(); @@ -8812,13 +8812,13 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); - info->wasArrayInterfaceDevirt = true; + info->needsMethodContext = true; } else { info->exactContext = MAKE_CLASSCONTEXT((CORINFO_CLASS_HANDLE) pExactMT); info->isInstantiatingStub = false; - info->wasArrayInterfaceDevirt = false; + info->needsMethodContext = false; } info->devirtualizedMethod = (CORINFO_METHOD_HANDLE) pDevirtMD; From 729b26952274b9dec83b83a31c4ffb0d6d4611b7 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 00:22:57 +0900 Subject: [PATCH 02/22] Devirtualize generic virtual methods --- src/coreclr/jit/gentree.cpp | 4 +- src/coreclr/jit/gentree.h | 5 ++ src/coreclr/jit/importercalls.cpp | 77 ++++++++++++++------- src/coreclr/jit/indirectcalltransformer.cpp | 5 +- src/coreclr/jit/inline.h | 2 +- src/coreclr/jit/jitconfigvalues.h | 3 + src/coreclr/vm/jitinterface.cpp | 24 +++++-- 7 files changed, 83 insertions(+), 37 deletions(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index fb008955730152..56d4e88be7cb60 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -2374,9 +2374,7 @@ int GenTreeCall::GetNonStandardAddedArgCount(Compiler* compiler) const // bool GenTreeCall::IsDevirtualizationCandidate(Compiler* compiler) const { - return IsVirtual() || - (gtCallType == CT_INDIRECT && (gtCallAddr->IsHelperCall(compiler, CORINFO_HELP_VIRTUAL_FUNC_PTR) || - gtCallAddr->IsHelperCall(compiler, CORINFO_HELP_GVMLOOKUP_FOR_SLOT))); + return IsVirtual() || IsGenericVirtual(compiler); } //------------------------------------------------------------------------- diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index bf5bbb2a3c3ff4..1a9b37a56af884 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -5269,6 +5269,11 @@ struct GenTreeCall final : public GenTree { return (gtFlags & GTF_CALL_VIRT_KIND_MASK) == GTF_CALL_VIRT_VTABLE; } + bool IsGenericVirtual(Compiler* compiler) const + { + return (gtCallType == CT_INDIRECT && (gtCallAddr->IsHelperCall(compiler, CORINFO_HELP_VIRTUAL_FUNC_PTR) || + gtCallAddr->IsHelperCall(compiler, CORINFO_HELP_GVMLOOKUP_FOR_SLOT))); + } bool IsDevirtualizationCandidate(Compiler* compiler) const; diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index b12258492ea612..370af272aa446d 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -451,7 +451,8 @@ var_types Compiler::impImportCall(OPCODE opcode, // Sine we are jumping over some code, check that its OK to skip that code assert((sig->callConv & CORINFO_CALLCONV_MASK) != CORINFO_CALLCONV_VARARG && (sig->callConv & CORINFO_CALLCONV_MASK) != CORINFO_CALLCONV_NATIVEVARARG); - goto DONE; + + goto DEVIRT; } case CORINFO_CALL: @@ -968,6 +969,8 @@ var_types Compiler::impImportCall(OPCODE opcode, } } +DEVIRT: + bool probing; probing = impConsiderCallProbe(call->AsCall(), rawILOffset); @@ -7627,11 +7630,11 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Iterate over the guesses for (int candidateId = 0; candidateId < candidatesCount; candidateId++) { - CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; - CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; - unsigned likelihood = likelihoods[candidateId]; - bool arrayInterface = false; - bool instantiatingStub = false; + CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; + CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; + unsigned likelihood = likelihoods[candidateId]; + bool needsMethodContext = false; + bool instantiatingStub = false; CORINFO_CONTEXT_HANDLE likelyContext = originalContext; @@ -7674,10 +7677,10 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, break; } - likelyContext = dvInfo.exactContext; - likelyMethod = dvInfo.devirtualizedMethod; - arrayInterface = dvInfo.needsMethodContext; - instantiatingStub = dvInfo.isInstantiatingStub; + likelyContext = dvInfo.exactContext; + likelyMethod = dvInfo.devirtualizedMethod; + needsMethodContext = dvInfo.needsMethodContext; + instantiatingStub = dvInfo.isInstantiatingStub; } else { @@ -7752,7 +7755,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Add this as a potential candidate. // addGuardedDevirtualizationCandidate(call, likelyMethod, likelyClass, likelyContext, likelyMethodAttribs, - likelyClassAttribs, likelihood, arrayInterface, instantiatingStub, + likelyClassAttribs, likelihood, needsMethodContext, instantiatingStub, baseMethod, originalContext); } } @@ -7778,7 +7781,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // methodAttr - attributes of the method // classAttr - attributes of the class // likelihood - odds that this class is the class seen at runtime -// arrayInterface - devirtualization of an array interface call +// needsMethodContext - devirtualized method needs generic method context (e.g. array interfaces, generic virtuals) // instantiatingStub - devirtualized method in an instantiating stub // originalMethodHandle - method handle of base method (before devirt) // originalContextHandle - context for the original call @@ -7790,7 +7793,7 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, unsigned methodAttr, unsigned classAttr, unsigned likelihood, - bool arrayInterface, + bool needsMethodContext, bool instantiatingStub, CORINFO_METHOD_HANDLE originalMethodHandle, CORINFO_CONTEXT_HANDLE originalContextHandle) @@ -7869,7 +7872,7 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, pInfo->originalContextHandle = originalContextHandle; pInfo->likelihood = likelihood; pInfo->exactContextHandle = contextHandle; - pInfo->arrayInterface = arrayInterface; + pInfo->needsMethodContext = needsMethodContext; // If the guarded method is an instantiating stub, find the instantiated method // @@ -8603,6 +8606,12 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, &rootCompiler->info.compMethodInfo->args); #endif // DEBUG + if (JitConfig.JitEnableGenericVirtualDevirtualization() == 0 && call->IsGenericVirtual(this)) + { + JITDUMP("\nimpDevirtualizeCall: generic virtual devirtualization disabled\n"); + return; + } + // Fetch information about the virtual method we're calling. CORINFO_METHOD_HANDLE baseMethod = *method; unsigned baseMethodAttribs = *methodFlags; @@ -8643,7 +8652,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // In non-R2R modes CALLVIRT will be turned into a // regular call+nullcheck by normal call importation. // - if ((baseMethodAttribs & CORINFO_FLG_VIRTUAL) == 0) + if (!call->IsGenericVirtual(this) && (baseMethodAttribs & CORINFO_FLG_VIRTUAL) == 0) { assert(call->IsVirtualStub()); assert(IsAot()); @@ -8737,7 +8746,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // It may or may not know enough to devirtualize... if (isInterface) { - assert(call->IsVirtualStub()); + assert(call->IsVirtualStub() || call->IsGenericVirtual(this)); JITDUMP("--- base class is interface\n"); } @@ -8795,17 +8804,18 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (dvInfo.isInstantiatingStub) { - // We should only end up with generic methods that needs a method context (eg. array interface). + // We should only end up with generic methods that needs a method context (eg. array interface, generic virtuals). // assert(dvInfo.needsMethodContext); // We don't expect NAOT to end up here, since it has Array // and normal devirtualization. + // As for generic virtual methods, NAOT uses fat pointers that we can't devirtualize yet. // assert(!IsTargetAbi(CORINFO_NATIVEAOT_ABI)); // We don't expect R2R to end up here, since it does not (yet) support - // array interface devirtualization. + // array interface devirtualization or generic virtual method devirtualization. // assert(!IsAot()); @@ -8818,12 +8828,12 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, return; } - // If we don't know the array type exactly we may have the wrong interface type here. + // If we don't know the exactly type we may have the wrong interface type here. // Bail out. // if (!isExact) { - JITDUMP("Array interface devirt: array type is inexact, sorry.\n"); + JITDUMP("%s devirt: type is inexact, sorry.\n", call->IsGenericVirtual(this) ? "Generic virtual method" : "Array interface"); return; } @@ -8927,7 +8937,24 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // Note different embedding would be needed for NAOT/R2R, // but we have ruled those out above. // - GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); + GenTree* instParam = nullptr; + // See if this is a generic virtual method call. + if (call->IsGenericVirtual(this)) + { + // If we have a RUNTIMELOOKUP helper call for the method handle, + // we need to pass that as the inst param instead. + GenTree* const methHndNode = + call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle)->GetEarlyNode(); + if (methHndNode->OperIs(GT_RUNTIMELOOKUP)) + { + instParam = methHndNode; + } + } + // For cases where we don't have a RUNTIMELOOKUP helper call, we pass the instantiating stub. + if (instParam == nullptr) + { + instParam = gtNewIconEmbHndNode(instantiatingStub, nullptr, GTF_ICON_METHOD_HDL, instantiatingStub); + } call->gtArgs.InsertInstParam(this, instParam); } @@ -9663,8 +9690,7 @@ void Compiler::impCheckCanInline(GenTreeCall* call, param.result = inlineResult; param.ppInlineCandidateInfo = ppInlineCandidateInfo; - bool success = eeRunWithErrorTrap( - [](Param* pParam) { + bool success = eeRunWithErrorTrap([](Param* pParam) { // Cache some frequently accessed state. // Compiler* const compiler = pParam->pThis; @@ -9798,7 +9824,7 @@ void Compiler::impCheckCanInline(GenTreeCall* call, pInfo->originalMethodHandle = nullptr; pInfo->originalContextHandle = nullptr; pInfo->likelihood = 0; - pInfo->arrayInterface = false; + pInfo->needsMethodContext = false; } pInfo->methInfo = methInfo; @@ -9817,8 +9843,7 @@ void Compiler::impCheckCanInline(GenTreeCall* call, // over in impMarkInlineCandidate. // *(pParam->ppInlineCandidateInfo) = pInfo; - }, - ¶m); + }, ¶m); if (!success) { diff --git a/src/coreclr/jit/indirectcalltransformer.cpp b/src/coreclr/jit/indirectcalltransformer.cpp index fd4ee8c5777908..b5f600365ecf73 100644 --- a/src/coreclr/jit/indirectcalltransformer.cpp +++ b/src/coreclr/jit/indirectcalltransformer.cpp @@ -970,10 +970,9 @@ class IndirectCallTransformer CORINFO_CONTEXT_HANDLE context = inlineInfo->exactContextHandle; if (clsHnd != NO_CLASS_HANDLE) { - // If we devirtualized an array interface call, - // pass the original method handle and original context handle to the devirtualizer. + // Pass the original method handle and original context handle to the devirtualizer if needed. // - if (inlineInfo->arrayInterface) + if (inlineInfo->needsMethodContext) { methodHnd = inlineInfo->originalMethodHandle; context = inlineInfo->originalContextHandle; diff --git a/src/coreclr/jit/inline.h b/src/coreclr/jit/inline.h index a89be81fa68895..fdad38be37ec8b 100644 --- a/src/coreclr/jit/inline.h +++ b/src/coreclr/jit/inline.h @@ -598,7 +598,7 @@ struct InlineCandidateInfo : public HandleHistogramProfileCandidateInfo CORINFO_METHOD_HANDLE guardedMethodUnboxedEntryHandle; CORINFO_METHOD_HANDLE guardedMethodInstantiatedEntryHandle; unsigned likelihood; - bool arrayInterface; + bool needsMethodContext; CORINFO_METHOD_INFO methInfo; diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index eb675224874412..b7b368f7779886 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -126,6 +126,9 @@ CONFIG_STRING(JitInlineMethodsWithEHRange, "JitInlineMethodsWithEHRange") CONFIG_INTEGER(JitLongAddress, "JitLongAddress", 0) // Force using the large pseudo instruction form for long address CONFIG_INTEGER(JitMaxUncheckedOffset, "JitMaxUncheckedOffset", 8) +// Enable devirtualization for generic virtual methods +CONFIG_INTEGER(JitEnableGenericVirtualDevirtualization, "JitEnableGenericVirtualDevirtualization", 1) + // // MinOpts // diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 47cd461cc0373b..fac4887bdc039c 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8576,9 +8576,6 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // Method better be from a fully loaded class _ASSERTE(pBaseMT->IsFullyLoaded()); - //@GENERICS: shouldn't be doing this for instantiated methods as they live elsewhere - _ASSERTE(!pBaseMD->HasMethodInstantiation()); - // Method better be virtual _ASSERTE(pBaseMD->IsVirtual()); @@ -8786,6 +8783,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) MethodTable* pApproxMT = pDevirtMD->GetMethodTable(); MethodTable* pExactMT = pApproxMT; bool isArray = false; + bool isGenericVirtual = false; if (pApproxMT->IsInterface()) { @@ -8802,10 +8800,28 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) { pExactMT = pDevirtMD->GetExactDeclaringType(pObjMT); } + + // This is generic virtual method devirtualization. + if (!isArray && pBaseMD->HasMethodInstantiation()) + { + // If we're in a shared context we'll devirt to a shared + // generic method and won't be able to inline, so just bail. + // + if (pExactMT->IsSharedByGenericInstantiations()) + { + info->detail = CORINFO_DEVIRTUALIZATION_FAILED_CANON; + return false; + } + + pDevirtMD = pDevirtMD->FindOrCreateAssociatedMethodDesc( + pDevirtMD, pExactMT, pExactMT->IsValueType() && !pDevirtMD->IsStatic(), pBaseMD->GetMethodInstantiation(), false); + + isGenericVirtual = true; + } // Success! Pass back the results. // - if (isArray) + if (isArray || isGenericVirtual) { // Note if array devirtualization produced an instantiation stub // so jit can try and inline it. From 5c15b23693c3b2f12573710b57c5d15b8c5702ef Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 01:58:36 +0900 Subject: [PATCH 03/22] Disable GVM devirt for AOT --- src/coreclr/jit/gentree.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 56d4e88be7cb60..d05f750d359108 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -2374,7 +2374,8 @@ int GenTreeCall::GetNonStandardAddedArgCount(Compiler* compiler) const // bool GenTreeCall::IsDevirtualizationCandidate(Compiler* compiler) const { - return IsVirtual() || IsGenericVirtual(compiler); + // TODO: Support devirtualization for AOT generic virtual calls. + return IsVirtual() || (!compiler->IsAot() && IsGenericVirtual(compiler)); } //------------------------------------------------------------------------- From 6e678a54612a39a5d29b60d3292ec51a70c15e4b Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 02:17:01 +0900 Subject: [PATCH 04/22] Unblock late devirt as well --- src/coreclr/jit/fginline.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index ec41df22af9671..e760d7cc25c888 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -613,8 +613,12 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitorOperIs(GT_CALL)) { GenTreeCall* call = tree->AsCall(); - // TODO-CQ: Drop `call->gtCallType == CT_USER_FUNC` once we have GVM devirtualization - bool tryLateDevirt = call->IsDevirtualizationCandidate(m_compiler) && (call->gtCallType == CT_USER_FUNC); + bool tryLateDevirt = call->IsDevirtualizationCandidate(m_compiler); + if (tryLateDevirt && call->gtCallType == CT_INDIRECT) + { + // For indirect calls, we can only late devirt if it's a generic virtual method for now. + tryLateDevirt = call->IsGenericVirtual(m_compiler); + } #ifdef DEBUG tryLateDevirt = tryLateDevirt && (JitConfig.JitEnableLateDevirtualization() == 1); From c488b849e315c83b3618ae6de95c429a7686e5c1 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 02:42:38 +0900 Subject: [PATCH 05/22] Set single-def --- src/coreclr/jit/importercalls.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 370af272aa446d..c36a5b056949fa 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -417,11 +417,20 @@ var_types Compiler::impImportCall(OPCODE opcode, thisPtr = impTransformThis(thisPtr, pConstrainedResolvedToken, callInfo->thisTransform); assert(thisPtr != nullptr); + GenTree* origThisPtr = thisPtr; + // Clone the (possibly transformed) "this" pointer GenTree* thisPtrCopy; thisPtr = impCloneExpr(thisPtr, &thisPtrCopy, CHECK_SPILL_ALL, nullptr DEBUGARG("LDVIRTFTN this pointer")); + // We cloned the "this" pointer, mark it as a single def and set the class for it + if (thisPtr->OperIsLocal() && thisPtr->TypeIs(TYP_REF) && (origThisPtr != thisPtr)) + { + lvaGetDesc(thisPtr->AsLclVarCommon())->lvSingleDef = 1; + lvaSetClass(thisPtr->AsLclVarCommon()->GetLclNum(), origThisPtr); + } + GenTree* fptr = impImportLdvirtftn(thisPtr, pResolvedToken, callInfo); assert(fptr != nullptr); @@ -8804,7 +8813,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (dvInfo.isInstantiatingStub) { - // We should only end up with generic methods that needs a method context (eg. array interface, generic virtuals). + // We should only end up with generic methods that needs a method context (eg. array interface, generic + // virtuals). // assert(dvInfo.needsMethodContext); @@ -8833,7 +8843,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // if (!isExact) { - JITDUMP("%s devirt: type is inexact, sorry.\n", call->IsGenericVirtual(this) ? "Generic virtual method" : "Array interface"); + JITDUMP("%s devirt: type is inexact, sorry.\n", + call->IsGenericVirtual(this) ? "Generic virtual method" : "Array interface"); return; } From 0ca8b93e92db94d432e8c05150f564f3565ec6a4 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 02:47:23 +0900 Subject: [PATCH 06/22] Address feedbacks --- src/coreclr/jit/importercalls.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index c36a5b056949fa..8aa86b74f46db3 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -457,7 +457,7 @@ var_types Compiler::impImportCall(OPCODE opcode, } #endif - // Sine we are jumping over some code, check that its OK to skip that code + // Since we are jumping over some code, check that its OK to skip that code assert((sig->callConv & CORINFO_CALLCONV_MASK) != CORINFO_CALLCONV_VARARG && (sig->callConv & CORINFO_CALLCONV_MASK) != CORINFO_CALLCONV_NATIVEVARARG); @@ -8838,7 +8838,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, return; } - // If we don't know the exactly type we may have the wrong interface type here. + // If we don't know the exact type we may have the wrong interface type here. // Bail out. // if (!isExact) @@ -8954,8 +8954,9 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, { // If we have a RUNTIMELOOKUP helper call for the method handle, // we need to pass that as the inst param instead. - GenTree* const methHndNode = - call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle)->GetEarlyNode(); + CallArg* const methHndArg = call->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methHndArg != nullptr); + GenTree* const methHndNode = methHndArg->GetEarlyNode(); if (methHndNode->OperIs(GT_RUNTIMELOOKUP)) { instParam = methHndNode; @@ -8964,7 +8965,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // For cases where we don't have a RUNTIMELOOKUP helper call, we pass the instantiating stub. if (instParam == nullptr) { - instParam = gtNewIconEmbHndNode(instantiatingStub, nullptr, GTF_ICON_METHOD_HDL, instantiatingStub); + instParam = gtNewIconEmbMethHndNode(instantiatingStub); } call->gtArgs.InsertInstParam(this, instParam); } From 0ac9d6d8aca152ad1ab3cdbc54d710b154c6a068 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 03:04:41 +0900 Subject: [PATCH 07/22] Meh --- src/coreclr/jit/jitconfigvalues.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index b7b368f7779886..813e3b46568111 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -127,7 +127,7 @@ CONFIG_INTEGER(JitLongAddress, "JitLongAddress", 0) // Force using the large pse CONFIG_INTEGER(JitMaxUncheckedOffset, "JitMaxUncheckedOffset", 8) // Enable devirtualization for generic virtual methods -CONFIG_INTEGER(JitEnableGenericVirtualDevirtualization, "JitEnableGenericVirtualDevirtualization", 1) +RELEASE_CONFIG_INTEGER(JitEnableGenericVirtualDevirtualization, "JitEnableGenericVirtualDevirtualization", 1) // // MinOpts From 8976d4a27b3cb33dd31f602e97dc1ae65b4929f2 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 03:05:40 +0900 Subject: [PATCH 08/22] JIT format --- src/coreclr/jit/fginline.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index e760d7cc25c888..23be52d72737db 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -612,8 +612,8 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitorOperIs(GT_CALL)) { - GenTreeCall* call = tree->AsCall(); - bool tryLateDevirt = call->IsDevirtualizationCandidate(m_compiler); + GenTreeCall* call = tree->AsCall(); + bool tryLateDevirt = call->IsDevirtualizationCandidate(m_compiler); if (tryLateDevirt && call->gtCallType == CT_INDIRECT) { // For indirect calls, we can only late devirt if it's a generic virtual method for now. @@ -1377,8 +1377,7 @@ void Compiler::fgInvokeInlineeCompiler(GenTreeCall* call, InlineResult* inlineRe param.fncHandle = fncHandle; param.inlineCandidateInfo = inlineCandidateInfo; param.inlineInfo = &inlineInfo; - bool success = eeRunWithErrorTrap( - [](Param* pParam) { + bool success = eeRunWithErrorTrap([](Param* pParam) { // Init the local var info of the inlinee pParam->pThis->impInlineInitVars(pParam->inlineInfo); @@ -1454,8 +1453,7 @@ void Compiler::fgInvokeInlineeCompiler(GenTreeCall* call, InlineResult* inlineRe } } } - }, - ¶m); + }, ¶m); if (!success) { #ifdef DEBUG From 8d0845e7d548873fb8969f5494aa6d47f5b58b15 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 28 Nov 2025 03:16:13 +0900 Subject: [PATCH 09/22] JIT format again --- src/coreclr/jit/fginline.cpp | 6 ++++-- src/coreclr/jit/importercalls.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 23be52d72737db..32bae4028fa5e1 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1377,7 +1377,8 @@ void Compiler::fgInvokeInlineeCompiler(GenTreeCall* call, InlineResult* inlineRe param.fncHandle = fncHandle; param.inlineCandidateInfo = inlineCandidateInfo; param.inlineInfo = &inlineInfo; - bool success = eeRunWithErrorTrap([](Param* pParam) { + bool success = eeRunWithErrorTrap( + [](Param* pParam) { // Init the local var info of the inlinee pParam->pThis->impInlineInitVars(pParam->inlineInfo); @@ -1453,7 +1454,8 @@ void Compiler::fgInvokeInlineeCompiler(GenTreeCall* call, InlineResult* inlineRe } } } - }, ¶m); + }, + ¶m); if (!success) { #ifdef DEBUG diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 8aa86b74f46db3..9cfa8431c2c935 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -9702,7 +9702,8 @@ void Compiler::impCheckCanInline(GenTreeCall* call, param.result = inlineResult; param.ppInlineCandidateInfo = ppInlineCandidateInfo; - bool success = eeRunWithErrorTrap([](Param* pParam) { + bool success = eeRunWithErrorTrap( + [](Param* pParam) { // Cache some frequently accessed state. // Compiler* const compiler = pParam->pThis; @@ -9855,7 +9856,8 @@ void Compiler::impCheckCanInline(GenTreeCall* call, // over in impMarkInlineCandidate. // *(pParam->ppInlineCandidateInfo) = pInfo; - }, ¶m); + }, + ¶m); if (!success) { From 1d389f1afad373be5931a861ac3041dbfdbba9fc Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 28 Nov 2025 09:24:24 +0900 Subject: [PATCH 10/22] Check methHndArg --- src/coreclr/jit/importercalls.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 9cfa8431c2c935..57ed662b012f89 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8954,10 +8954,9 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, { // 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); - assert(methHndArg != nullptr); - GenTree* const methHndNode = methHndArg->GetEarlyNode(); - if (methHndNode->OperIs(GT_RUNTIMELOOKUP)) + CallArg* const methHndArg = call->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + GenTree* const methHndNode = methHndArg != nullptr ? methHndArg->GetEarlyNode() : nullptr; + if (methHndNode && methHndNode->OperIs(GT_RUNTIMELOOKUP)) { instParam = methHndNode; } From 217c9f2f7a531c1dcb97ab8108ab42ef30d120be Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 29 Nov 2025 23:54:40 +0900 Subject: [PATCH 11/22] Rework how inst param being embedded --- src/coreclr/jit/fginline.cpp | 17 ++++++++++--- src/coreclr/jit/importercalls.cpp | 41 ++++++++++++++----------------- src/coreclr/vm/jitinterface.cpp | 11 +-------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 32bae4028fa5e1..5b1f058ea848b0 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -551,7 +551,7 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitorgtCallMethHnd; } - else + else if (call->IsGenericVirtual(m_compiler)) { GenTree* runtimeMethHndNode = call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle)->GetNode(); @@ -563,11 +563,12 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitorAsIntCon()->IconValue()); default: - assert(!"Unexpected type in RuntimeMethodHandle arg."); + // Unable to get method handle for devirtualization. + // This can happen if the method handle is not an RUNTIMELOOKUP or CNS_INT for generic virtuals, return nullptr; } - return nullptr; } + return nullptr; } //------------------------------------------------------------------------ @@ -641,6 +642,16 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitorIsTailPrefixedCall(); + if (method == nullptr) + { + assert(!call->IsVirtual()); + // Unable to get method handle for devirtualization. + // This can happen if the method handle is not an RUNTIMELOOKUP or CNS_INT for generic virtuals, + // e.g. a local variable holding the method handle. + // Just bail out. + return; + } + CORINFO_CONTEXT_HANDLE contextInput = context; context = nullptr; m_compiler->impDevirtualizeCall(call, nullptr, &method, &methodFlags, &contextInput, &context, diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 57ed662b012f89..30de176421613a 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8652,6 +8652,9 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, #endif // DEBUG } + // Is the call a generic virtual method call? + const bool isGenericVirtual = call->IsGenericVirtual(this); + // In R2R mode, we might see virtual stub calls to // non-virtuals. For instance cases where the non-virtual method // is in a different assembly but is called via CALLVIRT. For @@ -8661,7 +8664,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // In non-R2R modes CALLVIRT will be turned into a // regular call+nullcheck by normal call importation. // - if (!call->IsGenericVirtual(this) && (baseMethodAttribs & CORINFO_FLG_VIRTUAL) == 0) + if (!isGenericVirtual && (baseMethodAttribs & CORINFO_FLG_VIRTUAL) == 0) { assert(call->IsVirtualStub()); assert(IsAot()); @@ -8755,7 +8758,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // It may or may not know enough to devirtualize... if (isInterface) { - assert(call->IsVirtualStub() || call->IsGenericVirtual(this)); + assert(call->IsVirtualStub() || isGenericVirtual); JITDUMP("--- base class is interface\n"); } @@ -8844,7 +8847,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (!isExact) { JITDUMP("%s devirt: type is inexact, sorry.\n", - call->IsGenericVirtual(this) ? "Generic virtual method" : "Array interface"); + isGenericVirtual ? "Generic virtual method" : "Array interface"); return; } @@ -8941,33 +8944,27 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, JITDUMP(" %s; can devirtualize\n", note); - if (dvInfo.isInstantiatingStub) + if (!isGenericVirtual && dvInfo.isInstantiatingStub) { // Pass the instantiating stub method desc as the inst param arg. // // Note different embedding would be needed for NAOT/R2R, // but we have ruled those out above. // - GenTree* instParam = nullptr; - // See if this is a generic virtual method call. - if (call->IsGenericVirtual(this)) - { - // 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; - } - } - // For cases where we don't have a RUNTIMELOOKUP helper call, we pass the instantiating stub. - if (instParam == nullptr) - { - instParam = gtNewIconEmbMethHndNode(instantiatingStub); - } + GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); call->gtArgs.InsertInstParam(this, instParam); } + else if (isGenericVirtual) + { + assert(dvInfo.needsMethodContext); + assert(call->gtCallAddr->IsCall()); + // Pass the method context as the inst param arg. + // We may happen to already have an inst param arg in instantiatingStub, but always use the methodHnd here. + CallArg* const methHndArg = call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methHndArg != nullptr); + GenTree* const methHnd = methHndArg->GetEarlyNode(); + call->gtArgs.InsertInstParam(this, methHnd); + } // Make the updates. call->gtFlags &= ~GTF_CALL_VIRT_VTABLE; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index fac4887bdc039c..312c7915eb67e8 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8804,15 +8804,6 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // This is generic virtual method devirtualization. if (!isArray && pBaseMD->HasMethodInstantiation()) { - // If we're in a shared context we'll devirt to a shared - // generic method and won't be able to inline, so just bail. - // - if (pExactMT->IsSharedByGenericInstantiations()) - { - info->detail = CORINFO_DEVIRTUALIZATION_FAILED_CANON; - return false; - } - pDevirtMD = pDevirtMD->FindOrCreateAssociatedMethodDesc( pDevirtMD, pExactMT, pExactMT->IsValueType() && !pDevirtMD->IsStatic(), pBaseMD->GetMethodInstantiation(), false); @@ -8826,7 +8817,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // Note if array devirtualization produced an instantiation stub // so jit can try and inline it. // - info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); + info->isInstantiatingStub = !isGenericVirtual && pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); info->needsMethodContext = true; } From 4b64c2e1f923658fe1da4e0375ffe25573588e35 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 29 Nov 2025 23:59:45 +0900 Subject: [PATCH 12/22] Bail out GVM devirt in managed type system instead --- src/coreclr/inc/corinfo.h | 1 + src/coreclr/jit/compiler.cpp | 2 ++ src/coreclr/jit/gentree.cpp | 3 +-- src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs | 7 ++++++- src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs | 1 + 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 226cf8da1fe120..8904ed4bebe34f 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1556,6 +1556,7 @@ enum CORINFO_DEVIRTUALIZATION_DETAIL CORINFO_DEVIRTUALIZATION_FAILED_DUPLICATE_INTERFACE, // crossgen2 virtual method algorithm and runtime algorithm differ in the presence of duplicate interface implementations CORINFO_DEVIRTUALIZATION_FAILED_DECL_NOT_REPRESENTABLE, // Decl method cannot be represented in R2R image CORINFO_DEVIRTUALIZATION_FAILED_TYPE_EQUIVALENCE, // Support for type equivalence in devirtualization is not yet implemented in crossgen2 + CORINFO_DEVIRTUALIZATION_FAILED_GENERIC_VIRTUAL, // Devirtualization of generic virtual methods is not yet implemented in crossgen2 CORINFO_DEVIRTUALIZATION_COUNT, // sentinel for maximum value }; diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index e74188ffc43fef..1cd8dcc3bb9e64 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10598,6 +10598,8 @@ const char* Compiler::devirtualizationDetailToString(CORINFO_DEVIRTUALIZATION_DE return "Decl method cannot be represented in R2R image"; case CORINFO_DEVIRTUALIZATION_FAILED_TYPE_EQUIVALENCE: return "Support for type equivalence in devirtualization is not yet implemented in crossgen2"; + case CORINFO_DEVIRTUALIZATION_FAILED_GENERIC_VIRTUAL: + return "Devirtualization of generic virtual methods is not yet implemented in crossgen2"; default: return "undefined"; } diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index d05f750d359108..56d4e88be7cb60 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -2374,8 +2374,7 @@ int GenTreeCall::GetNonStandardAddedArgCount(Compiler* compiler) const // bool GenTreeCall::IsDevirtualizationCandidate(Compiler* compiler) const { - // TODO: Support devirtualization for AOT generic virtual calls. - return IsVirtual() || (!compiler->IsAot() && IsGenericVirtual(compiler)); + return IsVirtual() || IsGenericVirtual(compiler); } //------------------------------------------------------------------------- diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 0edb3ec0a4b88a..d77134334f256f 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1340,7 +1340,12 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info) // Transform from the unboxing thunk to the normal method decl = decl.IsUnboxingThunk() ? decl.GetUnboxedMethod() : decl; - Debug.Assert(!decl.HasInstantiation); + if (decl.HasInstantiation) + { + // We cannot devirtualize generic virtual methods in AOT yet + info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_GENERIC_VIRTUAL; + return false; + } if ((info->context != null) && decl.OwningType.IsInterface) { diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index e6d68d07dc0115..85cceb7e82197d 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -1136,6 +1136,7 @@ public enum CORINFO_DEVIRTUALIZATION_DETAIL CORINFO_DEVIRTUALIZATION_FAILED_DUPLICATE_INTERFACE, // crossgen2 virtual method algorithm and runtime algorithm differ in the presence of duplicate interface implementations CORINFO_DEVIRTUALIZATION_FAILED_DECL_NOT_REPRESENTABLE, // Decl method cannot be represented in R2R image CORINFO_DEVIRTUALIZATION_FAILED_TYPE_EQUIVALENCE, // Support for type equivalence in devirtualization is not yet implemented in crossgen2 + CORINFO_DEVIRTUALIZATION_FAILED_GENERIC_VIRTUAL, // Devirtualization of generic virtual methods is not yet implemented in crossgen2 CORINFO_DEVIRTUALIZATION_COUNT, // sentinel for maximum value } From e8668a1b0ed729cd4074f4357bdecef0e5611e1b Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 30 Nov 2025 00:04:14 +0900 Subject: [PATCH 13/22] Remove the out-dated comments --- src/coreclr/jit/importercalls.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 30de176421613a..f5387fcb762165 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8816,19 +8816,17 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (dvInfo.isInstantiatingStub) { - // We should only end up with generic methods that needs a method context (eg. array interface, generic - // virtuals). + // We should only end up with generic methods that needs a method context (eg. array interface). // assert(dvInfo.needsMethodContext); // We don't expect NAOT to end up here, since it has Array // and normal devirtualization. - // As for generic virtual methods, NAOT uses fat pointers that we can't devirtualize yet. // assert(!IsTargetAbi(CORINFO_NATIVEAOT_ABI)); // We don't expect R2R to end up here, since it does not (yet) support - // array interface devirtualization or generic virtual method devirtualization. + // array interface devirtualization. // assert(!IsAot()); @@ -8841,13 +8839,12 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, return; } - // If we don't know the exact type we may have the wrong interface type here. + // If we don't know the array type exactly we may have the wrong interface type here. // Bail out. // if (!isExact) { - JITDUMP("%s devirt: type is inexact, sorry.\n", - isGenericVirtual ? "Generic virtual method" : "Array interface"); + JITDUMP("Array interface devirt: array type is inexact, sorry.\n"); return; } @@ -8959,7 +8956,6 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, assert(dvInfo.needsMethodContext); assert(call->gtCallAddr->IsCall()); // Pass the method context as the inst param arg. - // We may happen to already have an inst param arg in instantiatingStub, but always use the methodHnd here. CallArg* const methHndArg = call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); assert(methHndArg != nullptr); GenTree* const methHnd = methHndArg->GetEarlyNode(); From 951c4ea7ee5eb3fac3d1ff6d432ae4290d838022 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 30 Nov 2025 00:14:39 +0900 Subject: [PATCH 14/22] JIT format --- src/coreclr/jit/importercalls.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index f5387fcb762165..c03a92bdd79642 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8956,7 +8956,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, assert(dvInfo.needsMethodContext); assert(call->gtCallAddr->IsCall()); // Pass the method context as the inst param arg. - CallArg* const methHndArg = call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + CallArg* const methHndArg = + call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); assert(methHndArg != nullptr); GenTree* const methHnd = methHndArg->GetEarlyNode(); call->gtArgs.InsertInstParam(this, methHnd); From a1d605fc7a6cfc51023e39cd51539a87650492e1 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 30 Nov 2025 02:51:11 +0900 Subject: [PATCH 15/22] generic context is not always needed --- src/coreclr/jit/importercalls.cpp | 5 ++--- src/coreclr/vm/jitinterface.cpp | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index c03a92bdd79642..71d8cfad69f7f1 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8795,7 +8795,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, { // Array interface devirt can return a nonvirtual generic method of the non-generic SZArrayHelper class. // - assert(dvInfo.needsMethodContext); + assert(isGenericVirtual || dvInfo.needsMethodContext); assert(((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD); derivedClass = info.compCompHnd->getMethodClass(derivedMethod); } @@ -8951,9 +8951,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); call->gtArgs.InsertInstParam(this, instParam); } - else if (isGenericVirtual) + else if (isGenericVirtual && dvInfo.needsMethodContext) { - assert(dvInfo.needsMethodContext); assert(call->gtCallAddr->IsCall()); // Pass the method context as the inst param arg. CallArg* const methHndArg = diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 312c7915eb67e8..cef929a1ff5dce 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8819,7 +8819,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // info->isInstantiatingStub = !isGenericVirtual && pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); - info->needsMethodContext = true; + info->needsMethodContext = isArray || (isGenericVirtual && pDevirtMD->RequiresInstArg()); } else { From a941aec0f091b3e4a68e5d3ae4e98f984c86dc11 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 30 Nov 2025 03:14:23 +0900 Subject: [PATCH 16/22] Check isInstantiatingStub for the necessity of generic method context --- src/coreclr/jit/importercalls.cpp | 43 +++++++++++++++++-------------- src/coreclr/vm/jitinterface.cpp | 4 +-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 71d8cfad69f7f1..591163217150b9 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8814,7 +8814,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, CORINFO_METHOD_HANDLE instantiatingStub = NO_METHOD_HANDLE; - if (dvInfo.isInstantiatingStub) + if (dvInfo.isInstantiatingStub && !isGenericVirtual) { // We should only end up with generic methods that needs a method context (eg. array interface). // @@ -8941,25 +8941,30 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, JITDUMP(" %s; can devirtualize\n", note); - if (!isGenericVirtual && dvInfo.isInstantiatingStub) + if (dvInfo.isInstantiatingStub) { - // Pass the instantiating stub method desc as the inst param arg. - // - // Note different embedding would be needed for NAOT/R2R, - // but we have ruled those out above. - // - GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); - call->gtArgs.InsertInstParam(this, instParam); - } - else if (isGenericVirtual && dvInfo.needsMethodContext) - { - assert(call->gtCallAddr->IsCall()); - // Pass the method context as the inst param arg. - CallArg* const methHndArg = - call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); - assert(methHndArg != nullptr); - GenTree* const methHnd = methHndArg->GetEarlyNode(); - call->gtArgs.InsertInstParam(this, methHnd); + if (isGenericVirtual) + { + // This generic method call requires a generic method context. + // Pass the method handle as the inst param arg. + // + assert(call->gtCallAddr->IsCall()); + CallArg* const methHndArg = + call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methHndArg != nullptr); + GenTree* const methHnd = methHndArg->GetEarlyNode(); + call->gtArgs.InsertInstParam(this, methHnd); + } + else + { + // Pass the instantiating stub method desc as the inst param arg. + // + // Note different embedding would be needed for NAOT/R2R, + // but we have ruled those out above. + // + GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); + call->gtArgs.InsertInstParam(this, instParam); + } } // Make the updates. diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index cef929a1ff5dce..97f3fde2126526 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8817,9 +8817,9 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // Note if array devirtualization produced an instantiation stub // so jit can try and inline it. // - info->isInstantiatingStub = !isGenericVirtual && pDevirtMD->IsInstantiatingStub(); + info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); - info->needsMethodContext = isArray || (isGenericVirtual && pDevirtMD->RequiresInstArg()); + info->needsMethodContext = true; } else { From 9dd04f0a81fc597df70a6ac2b9022fd438fd5f0a Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 30 Nov 2025 23:34:15 +0900 Subject: [PATCH 17/22] Minor refactor --- src/coreclr/inc/corinfo.h | 4 +- src/coreclr/jit/importercalls.cpp | 37 ++++++++++--------- src/coreclr/jit/indirectcalltransformer.cpp | 2 +- src/coreclr/jit/inline.h | 2 +- .../tools/Common/JitInterface/CorInfoImpl.cs | 2 +- .../tools/Common/JitInterface/CorInfoTypes.cs | 6 +-- .../tools/superpmi/superpmi-shared/agnostic.h | 2 +- .../superpmi-shared/methodcontext.cpp | 8 ++-- src/coreclr/vm/jitinterface.cpp | 14 +++++-- 9 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 8904ed4bebe34f..f2c5f044791696 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1579,7 +1579,7 @@ struct CORINFO_DEVIRTUALIZATION_INFO // - If pResolvedTokenDevirtualizedMethod is not set to NULL and targeting an R2R image // use it as the parameter to getCallInfo // - isInstantiatingStub is set to TRUE if the devirtualized method is a generic method instantiating stub - // - needsMethodContext is set TRUE if the devirtualized method requires a method context + // - mayNeedMethodContext is set TRUE if the devirtualized method may require a method context // (in which case the method handle and context will be a generic method) // CORINFO_METHOD_HANDLE devirtualizedMethod; @@ -1588,7 +1588,7 @@ struct CORINFO_DEVIRTUALIZATION_INFO CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedMethod; CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedUnboxedMethod; bool isInstantiatingStub; - bool needsMethodContext; + bool mayNeedMethodContext; }; //---------------------------------------------------------------------------- diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 591163217150b9..0053ba8fe21ad0 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7615,7 +7615,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, } addGuardedDevirtualizationCandidate(call, exactMethod, exactCls, exactContext, exactMethodAttrs, - clsAttrs, likelyHood, dvInfo.needsMethodContext, + clsAttrs, likelyHood, dvInfo.mayNeedMethodContext, dvInfo.isInstantiatingStub, baseMethod, originalContext); } @@ -7639,11 +7639,11 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Iterate over the guesses for (int candidateId = 0; candidateId < candidatesCount; candidateId++) { - CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; - CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; - unsigned likelihood = likelihoods[candidateId]; - bool needsMethodContext = false; - bool instantiatingStub = false; + CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; + CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; + unsigned likelihood = likelihoods[candidateId]; + bool mayNeedMethodContext = false; + bool instantiatingStub = false; CORINFO_CONTEXT_HANDLE likelyContext = originalContext; @@ -7686,10 +7686,10 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, break; } - likelyContext = dvInfo.exactContext; - likelyMethod = dvInfo.devirtualizedMethod; - needsMethodContext = dvInfo.needsMethodContext; - instantiatingStub = dvInfo.isInstantiatingStub; + likelyContext = dvInfo.exactContext; + likelyMethod = dvInfo.devirtualizedMethod; + mayNeedMethodContext = dvInfo.mayNeedMethodContext; + instantiatingStub = dvInfo.isInstantiatingStub; } else { @@ -7764,7 +7764,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Add this as a potential candidate. // addGuardedDevirtualizationCandidate(call, likelyMethod, likelyClass, likelyContext, likelyMethodAttribs, - likelyClassAttribs, likelihood, needsMethodContext, instantiatingStub, + likelyClassAttribs, likelihood, mayNeedMethodContext, instantiatingStub, baseMethod, originalContext); } } @@ -7790,7 +7790,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // methodAttr - attributes of the method // classAttr - attributes of the class // likelihood - odds that this class is the class seen at runtime -// needsMethodContext - devirtualized method needs generic method context (e.g. array interfaces, generic virtuals) +// mayNeedMethodContext - devirtualized method may need generic method context (e.g. array interfaces, shared generic virtuals) // instantiatingStub - devirtualized method in an instantiating stub // originalMethodHandle - method handle of base method (before devirt) // originalContextHandle - context for the original call @@ -7802,7 +7802,7 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, unsigned methodAttr, unsigned classAttr, unsigned likelihood, - bool needsMethodContext, + bool mayNeedMethodContext, bool instantiatingStub, CORINFO_METHOD_HANDLE originalMethodHandle, CORINFO_CONTEXT_HANDLE originalContextHandle) @@ -7881,7 +7881,7 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, pInfo->originalContextHandle = originalContextHandle; pInfo->likelihood = likelihood; pInfo->exactContextHandle = contextHandle; - pInfo->needsMethodContext = needsMethodContext; + pInfo->mayNeedMethodContext = mayNeedMethodContext; // If the guarded method is an instantiating stub, find the instantiated method // @@ -8788,14 +8788,14 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_CLASS) { - assert(!dvInfo.needsMethodContext); + assert(!dvInfo.mayNeedMethodContext); derivedClass = (CORINFO_CLASS_HANDLE)((size_t)exactContext & ~CORINFO_CONTEXTFLAGS_MASK); } else { // Array interface devirt can return a nonvirtual generic method of the non-generic SZArrayHelper class. // - assert(isGenericVirtual || dvInfo.needsMethodContext); + assert(isGenericVirtual || dvInfo.mayNeedMethodContext); assert(((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD); derivedClass = info.compCompHnd->getMethodClass(derivedMethod); } @@ -8818,7 +8818,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, { // We should only end up with generic methods that needs a method context (eg. array interface). // - assert(dvInfo.needsMethodContext); + assert(dvInfo.mayNeedMethodContext); // We don't expect NAOT to end up here, since it has Array // and normal devirtualization. @@ -8948,6 +8948,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, // This generic method call requires a generic method context. // Pass the method handle as the inst param arg. // + assert(dvInfo.mayNeedMethodContext); assert(call->gtCallAddr->IsCall()); CallArg* const methHndArg = call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); @@ -9834,7 +9835,7 @@ void Compiler::impCheckCanInline(GenTreeCall* call, pInfo->originalMethodHandle = nullptr; pInfo->originalContextHandle = nullptr; pInfo->likelihood = 0; - pInfo->needsMethodContext = false; + pInfo->mayNeedMethodContext = false; } pInfo->methInfo = methInfo; diff --git a/src/coreclr/jit/indirectcalltransformer.cpp b/src/coreclr/jit/indirectcalltransformer.cpp index b5f600365ecf73..314f2102c6c5ad 100644 --- a/src/coreclr/jit/indirectcalltransformer.cpp +++ b/src/coreclr/jit/indirectcalltransformer.cpp @@ -972,7 +972,7 @@ class IndirectCallTransformer { // Pass the original method handle and original context handle to the devirtualizer if needed. // - if (inlineInfo->needsMethodContext) + if (inlineInfo->mayNeedMethodContext) { methodHnd = inlineInfo->originalMethodHandle; context = inlineInfo->originalContextHandle; diff --git a/src/coreclr/jit/inline.h b/src/coreclr/jit/inline.h index fdad38be37ec8b..fcada89e922cc5 100644 --- a/src/coreclr/jit/inline.h +++ b/src/coreclr/jit/inline.h @@ -598,7 +598,7 @@ struct InlineCandidateInfo : public HandleHistogramProfileCandidateInfo CORINFO_METHOD_HANDLE guardedMethodUnboxedEntryHandle; CORINFO_METHOD_HANDLE guardedMethodInstantiatedEntryHandle; unsigned likelihood; - bool needsMethodContext; + bool mayNeedMethodContext; CORINFO_METHOD_INFO methInfo; diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index d77134334f256f..bad1dcc085d06f 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1324,7 +1324,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info) info->exactContext = null; info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_UNKNOWN; info->isInstantiatingStub = false; - info->needsMethodContext = false; + info->mayNeedMethodContext = false; TypeDesc objType = HandleToObject(info->objClass); diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index 85cceb7e82197d..e12d4875a1d667 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -1157,7 +1157,7 @@ public unsafe struct CORINFO_DEVIRTUALIZATION_INFO // - exactContext is set to wrapped CORINFO_CLASS_HANDLE of devirt'ed method table. // - detail describes the computation done by the jit host // - isInstantiatingStub is set to TRUE if the devirtualized method is a method instantiation stub - // - needsMethodContext is set TRUE if the devirtualized method requires a method context + // - mayNeedMethodContext is set TRUE if the devirtualized method may require a method context // (in which case the method handle and context will be a generic method) // public CORINFO_METHOD_STRUCT_* devirtualizedMethod; @@ -1167,8 +1167,8 @@ public unsafe struct CORINFO_DEVIRTUALIZATION_INFO public CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedUnboxedMethod; public byte _isInstantiatingStub; public bool isInstantiatingStub { get { return _isInstantiatingStub != 0; } set { _isInstantiatingStub = value ? (byte)1 : (byte)0; } } - public byte _needsMethodContext; - public bool needsMethodContext { get { return _needsMethodContext != 0; } set { _needsMethodContext = value ? (byte)1 : (byte)0; } } + public byte _mayNeedMethodContext; + public bool mayNeedMethodContext { get { return _mayNeedMethodContext != 0; } set { _mayNeedMethodContext = value ? (byte)1 : (byte)0; } } } //---------------------------------------------------------------------------- diff --git a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h index 4dd74b117a25d5..a3670db34403ee 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h @@ -681,7 +681,7 @@ struct Agnostic_ResolveVirtualMethodResult bool returnValue; DWORDLONG devirtualizedMethod; bool isInstantiatingStub; - bool needsMethodContext; + bool mayNeedMethodContext; DWORDLONG exactContext; DWORD detail; Agnostic_CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedMethod; diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp index b7c59485f2af2e..e2f5090d4ad05f 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp @@ -3276,7 +3276,7 @@ void MethodContext::recResolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO * info result.isInstantiatingStub = info->isInstantiatingStub; result.exactContext = CastHandle(info->exactContext); result.detail = (DWORD) info->detail; - result.needsMethodContext = info->needsMethodContext; + result.mayNeedMethodContext = info->mayNeedMethodContext; if (returnValue) { @@ -3301,11 +3301,11 @@ void MethodContext::dmpResolveVirtualMethod(const Agnostic_ResolveVirtualMethodK key.context, key.pResolvedTokenVirtualMethodNonNull, key.pResolvedTokenVirtualMethodNonNull ? SpmiDumpHelper::DumpAgnostic_CORINFO_RESOLVED_TOKEN(key.pResolvedTokenVirtualMethod).c_str() : "???"); - printf(", value returnValue-%s, devirtMethod-%016" PRIX64 ", instantiatingStub-%s, needsMethodContext-%s, exactContext-%016" PRIX64 ", detail-%d, tokDvMeth{%s}, tokDvUnboxMeth{%s}", + printf(", value returnValue-%s, devirtMethod-%016" PRIX64 ", instantiatingStub-%s, mayNeedMethodContext-%s, exactContext-%016" PRIX64 ", detail-%d, tokDvMeth{%s}, tokDvUnboxMeth{%s}", result.returnValue ? "true" : "false", result.devirtualizedMethod, result.isInstantiatingStub ? "true" : "false", - result.needsMethodContext ? "true" : "false", + result.mayNeedMethodContext ? "true" : "false", result.exactContext, result.detail, result.returnValue ? SpmiDumpHelper::DumpAgnostic_CORINFO_RESOLVED_TOKEN(result.resolvedTokenDevirtualizedMethod).c_str() : "???", @@ -3330,7 +3330,7 @@ bool MethodContext::repResolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO * info info->devirtualizedMethod = (CORINFO_METHOD_HANDLE) result.devirtualizedMethod; info->isInstantiatingStub = result.isInstantiatingStub; - info->needsMethodContext = result.needsMethodContext; + info->mayNeedMethodContext = result.mayNeedMethodContext; info->exactContext = (CORINFO_CONTEXT_HANDLE) result.exactContext; info->detail = (CORINFO_DEVIRTUALIZATION_DETAIL) result.detail; if (result.returnValue) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 97f3fde2126526..93096ee858a296 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8568,7 +8568,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) memset(&info->resolvedTokenDevirtualizedMethod, 0, sizeof(info->resolvedTokenDevirtualizedMethod)); memset(&info->resolvedTokenDevirtualizedUnboxedMethod, 0, sizeof(info->resolvedTokenDevirtualizedUnboxedMethod)); info->isInstantiatingStub = false; - info->needsMethodContext = false; + info->mayNeedMethodContext = false; MethodDesc* pBaseMD = GetMethod(info->virtualMethod); MethodTable* pBaseMT = pBaseMD->GetMethodTable(); @@ -8812,20 +8812,26 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) // Success! Pass back the results. // - if (isArray || isGenericVirtual) + if (isArray) { // Note if array devirtualization produced an instantiation stub // so jit can try and inline it. // info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); - info->needsMethodContext = true; + info->mayNeedMethodContext = true; + } + else if (isGenericVirtual) + { + // We only need the method context if the devirted generic method is a shared generic method + info->mayNeedMethodContext = info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); + info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); } else { info->exactContext = MAKE_CLASSCONTEXT((CORINFO_CLASS_HANDLE) pExactMT); info->isInstantiatingStub = false; - info->needsMethodContext = false; + info->mayNeedMethodContext = false; } info->devirtualizedMethod = (CORINFO_METHOD_HANDLE) pDevirtMD; From e84df699a4f607fb791964e251505e99e98a8e81 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 1 Dec 2025 00:38:44 +0900 Subject: [PATCH 18/22] Use the wrapper entrypoint instead of the instantiating stub Otherwise we will emit a call to the instantiated method directly instead of a call to the wrapper entrypoint of a shared generic method (the canonical generic method) --- src/coreclr/jit/importercalls.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 0053ba8fe21ad0..c36f3c510ad686 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8814,9 +8814,9 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, CORINFO_METHOD_HANDLE instantiatingStub = NO_METHOD_HANDLE; - if (dvInfo.isInstantiatingStub && !isGenericVirtual) + if (dvInfo.isInstantiatingStub) { - // We should only end up with generic methods that needs a method context (eg. array interface). + // We should only end up with generic methods that needs a method context (eg. array interface, GVM). // assert(dvInfo.mayNeedMethodContext); From 17392c0990570f6020cab7f470d7fa1ae402b14f Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 1 Dec 2025 00:58:09 +0900 Subject: [PATCH 19/22] JIT format --- src/coreclr/jit/importercalls.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index c36f3c510ad686..89af946947f01f 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7790,7 +7790,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // methodAttr - attributes of the method // classAttr - attributes of the class // likelihood - odds that this class is the class seen at runtime -// mayNeedMethodContext - devirtualized method may need generic method context (e.g. array interfaces, shared generic virtuals) +// mayNeedMethodContext - devirtualized method may need generic method context (e.g. array interfaces) // instantiatingStub - devirtualized method in an instantiating stub // originalMethodHandle - method handle of base method (before devirt) // originalContextHandle - context for the original call From 9847c97f63cd70803749416b65115e6821edacdb Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 1 Dec 2025 01:35:07 +0900 Subject: [PATCH 20/22] Always use instantiatingStub --- src/coreclr/jit/importercalls.cpp | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 89af946947f01f..28c912e560cf3e 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8943,29 +8943,13 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (dvInfo.isInstantiatingStub) { - if (isGenericVirtual) - { - // This generic method call requires a generic method context. - // Pass the method handle as the inst param arg. - // - assert(dvInfo.mayNeedMethodContext); - assert(call->gtCallAddr->IsCall()); - CallArg* const methHndArg = - call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); - assert(methHndArg != nullptr); - GenTree* const methHnd = methHndArg->GetEarlyNode(); - call->gtArgs.InsertInstParam(this, methHnd); - } - else - { - // Pass the instantiating stub method desc as the inst param arg. - // - // Note different embedding would be needed for NAOT/R2R, - // but we have ruled those out above. - // - GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); - call->gtArgs.InsertInstParam(this, instParam); - } + // Pass the instantiating stub method desc as the inst param arg. + // + // Note different embedding would be needed for NAOT/R2R, + // but we have ruled those out above. + // + GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); + call->gtArgs.InsertInstParam(this, instParam); } // Make the updates. From 7896ef52aeab77721c6594f4e28f8918c48424e6 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 1 Dec 2025 02:47:01 +0900 Subject: [PATCH 21/22] Properly handle shared generics on both class and method --- src/coreclr/jit/importercalls.cpp | 39 +++++++++++++++++++++++++------ src/coreclr/vm/jitinterface.cpp | 16 +++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 28c912e560cf3e..8399c3cce26254 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8943,13 +8943,38 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, if (dvInfo.isInstantiatingStub) { - // Pass the instantiating stub method desc as the inst param arg. - // - // Note different embedding would be needed for NAOT/R2R, - // but we have ruled those out above. - // - GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); - call->gtArgs.InsertInstParam(this, instParam); + if (isGenericVirtual) + { + // This generic method call requires a generic method context. + // Pass the method handle as the inst param arg. + // + assert(call->gtCallAddr->IsCall()); + CallArg* const methHndArg = + call->gtCallAddr->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methHndArg != nullptr); + GenTree* methHnd = methHndArg->GetEarlyNode(); + if (methHnd->OperIs(GT_RUNTIMELOOKUP)) + { + // We need to do runtime lookup instead of using the instantiating stub directly. + call->gtArgs.InsertInstParam(this, methHnd); + } + else + { + // We can pass the instantiating stub directly as we don't need a runtime lookup. + GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); + call->gtArgs.InsertInstParam(this, instParam); + } + } + else + { + // Pass the instantiating stub method desc as the inst param arg. + // + // Note different embedding would be needed for NAOT/R2R, + // but we have ruled those out above. + // + GenTree* const instParam = gtNewIconEmbMethHndNode(instantiatingStub); + call->gtArgs.InsertInstParam(this, instParam); + } } // Make the updates. diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 93096ee858a296..91a35cc76eedcc 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8806,6 +8806,22 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) { pDevirtMD = pDevirtMD->FindOrCreateAssociatedMethodDesc( pDevirtMD, pExactMT, pExactMT->IsValueType() && !pDevirtMD->IsStatic(), pBaseMD->GetMethodInstantiation(), false); + + // If the generic virtual method requires a runtime lookup, and we devirted to a class that also + // requires a runtime lookup, we can't handle this case yet because we don't have the right generic context. + // For example: Base.M devirted to Derived.M, where both T and U are shared generic. + // In this case we would need both the T and U be part of the context to be able to do the lookup. + // TODO-CQ: Fix this limitation. + if (pDevirtMD->IsInstantiatingStub()) + { + MethodDesc* instantiatingStub = pDevirtMD->GetWrappedMethodDesc(); + if (instantiatingStub->GetMethodTable()->IsSharedByGenericInstantiations() + && instantiatingStub->IsSharedByGenericMethodInstantiations()) + { + info->detail = CORINFO_DEVIRTUALIZATION_FAILED_CANON; + return false; + } + } isGenericVirtual = true; } From 905b11d3696ba03e516481ef026b95772ba999b4 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 1 Dec 2025 03:06:41 +0900 Subject: [PATCH 22/22] Update comments --- src/coreclr/vm/jitinterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 91a35cc76eedcc..51eb3a54b5bb87 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -8839,7 +8839,7 @@ bool CEEInfo::resolveVirtualMethodHelper(CORINFO_DEVIRTUALIZATION_INFO * info) } else if (isGenericVirtual) { - // We only need the method context if the devirted generic method is a shared generic method + // We only need the method context when the devirted generic method is a shared generic method info->mayNeedMethodContext = info->isInstantiatingStub = pDevirtMD->IsInstantiatingStub(); info->exactContext = MAKE_METHODCONTEXT((CORINFO_METHOD_HANDLE) pDevirtMD); }