From 379885ae8f4515a3aff93f9c4829f3d3852f4d25 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 02:25:37 +0900 Subject: [PATCH 1/6] Profile class and method for GDV --- src/coreclr/inc/corinfo.h | 2 + src/coreclr/inc/jiteeversionguid.h | 10 +-- src/coreclr/inc/jithelpers.h | 4 + src/coreclr/jit/fgprofile.cpp | 84 ++++++++++++++----- src/coreclr/jit/importercalls.cpp | 36 ++++---- src/coreclr/jit/indirectcalltransformer.cpp | 48 ++++++++++- src/coreclr/jit/jitconfigvalues.h | 11 +-- src/coreclr/jit/morph.cpp | 3 +- .../Common/JitInterface/CorInfoHelpFunc.cs | 2 + src/coreclr/vm/jithelpers.cpp | 56 +++++++++++++ 10 files changed, 199 insertions(+), 57 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index dee4aae951532c..e6cced809dd672 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -577,6 +577,8 @@ enum CorInfoHelpFunc CORINFO_HELP_DELEGATEPROFILE64, // Update 64-bit method profile for a delegate call site CORINFO_HELP_VTABLEPROFILE32, // Update 32-bit method profile for a vtable call site CORINFO_HELP_VTABLEPROFILE64, // Update 64-bit method profile for a vtable call site + CORINFO_HELP_GENERICVIRTUALPROFILE32, // Update 32-bit method profile for a generic virtual call site + CORINFO_HELP_GENERICVIRTUALPROFILE64, // Update 64-bit method profile for a generic virtual call site CORINFO_HELP_COUNTPROFILE32, // Update 32-bit block or edge count profile CORINFO_HELP_COUNTPROFILE64, // Update 64-bit block or edge count profile CORINFO_HELP_VALUEPROFILE32, // Update 32-bit value profile diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index 8be933e939e508..b1a85479d90fbe 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* a0017f91-c35d-4a2b-94a4-03d89f63e12b */ - 0xa0017f91, - 0xc35d, - 0x4a2b, - {0x94, 0xa4, 0x03, 0xd8, 0x9f, 0x63, 0xe1, 0x2b} +constexpr GUID JITEEVersionIdentifier = { /* c31fbc28-2e59-4770-b836-61c690978dc2 */ + 0xc31fbc28, + 0x2e59, + 0x4770, + {0xb8, 0x36, 0x61, 0xc6, 0x90, 0x97, 0x8d, 0xc2} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/inc/jithelpers.h b/src/coreclr/inc/jithelpers.h index 7742945b23e7e4..d5756afeaadbbb 100644 --- a/src/coreclr/inc/jithelpers.h +++ b/src/coreclr/inc/jithelpers.h @@ -336,6 +336,8 @@ JITHELPER(CORINFO_HELP_DELEGATEPROFILE64, JIT_DelegateProfile64, METHOD__NIL) JITHELPER(CORINFO_HELP_VTABLEPROFILE32, JIT_VTableProfile32, METHOD__NIL) JITHELPER(CORINFO_HELP_VTABLEPROFILE64, JIT_VTableProfile64, METHOD__NIL) + JITHELPER(CORINFO_HELP_GENERICVIRTUALPROFILE32, JIT_GenericVirtualProfile32, METHOD__NIL) + JITHELPER(CORINFO_HELP_GENERICVIRTUALPROFILE64, JIT_GenericVirtualProfile64, METHOD__NIL) JITHELPER(CORINFO_HELP_COUNTPROFILE32, JIT_CountProfile32, METHOD__NIL) JITHELPER(CORINFO_HELP_COUNTPROFILE64, JIT_CountProfile64, METHOD__NIL) JITHELPER(CORINFO_HELP_VALUEPROFILE32, JIT_ValueProfile32, METHOD__NIL) @@ -347,6 +349,8 @@ JITHELPER(CORINFO_HELP_DELEGATEPROFILE64, NULL, METHOD__NIL) JITHELPER(CORINFO_HELP_VTABLEPROFILE32, NULL, METHOD__NIL) JITHELPER(CORINFO_HELP_VTABLEPROFILE64, NULL, METHOD__NIL) + JITHELPER(CORINFO_HELP_GENERICVIRTUALPROFILE32, NULL, METHOD__NIL) + JITHELPER(CORINFO_HELP_GENERICVIRTUALPROFILE64, NULL, METHOD__NIL) JITHELPER(CORINFO_HELP_COUNTPROFILE32, NULL, METHOD__NIL) JITHELPER(CORINFO_HELP_COUNTPROFILE64, NULL, METHOD__NIL) JITHELPER(CORINFO_HELP_VALUEPROFILE32, NULL, METHOD__NIL) diff --git a/src/coreclr/jit/fgprofile.cpp b/src/coreclr/jit/fgprofile.cpp index 46c3077ec6b6f9..68f264dad01937 100644 --- a/src/coreclr/jit/fgprofile.cpp +++ b/src/coreclr/jit/fgprofile.cpp @@ -1968,7 +1968,9 @@ class BuildHandleHistogramProbeSchemaGen ICorJitInfo::PgoInstrumentationSchema schemaElem = {}; schemaElem.Count = 1; schemaElem.Other = isTypeHistogram ? ICorJitInfo::HandleHistogram32::CLASS_FLAG : 0; - if (call->IsVirtualStub()) + if (call->IsVirtualStub() || + (call->IsGenericVirtual(compiler) && + ((call->gtControlExpr->AsCall()->gtCallMoreFlags & GTF_CALL_M_LDVIRTFTN_INTERFACE) != 0))) { schemaElem.Other |= ICorJitInfo::HandleHistogram32::INTERFACE_FLAG; } @@ -2115,32 +2117,67 @@ class HandleHistogramProbeInserter if (methodHistogram != nullptr) { - GenTree* const tmpNode = compiler->gtNewLclvNode(tmpNum, TYP_REF); - GenTree* const methodProfileNode = compiler->gtNewIconNode((ssize_t)methodHistogram, TYP_I_IMPL); - - GenTree* methodProfileCallNode; - if (call->IsDelegateInvoke()) + if (call->IsGenericVirtual(compiler)) { - methodProfileCallNode = compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_DELEGATEPROFILE32 - : CORINFO_HELP_DELEGATEPROFILE64, - TYP_VOID, tmpNode, methodProfileNode); + GenTreeCall* const lookupCall = call->gtControlExpr->AsCall(); + assert(lookupCall->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)); + assert(!lookupCall->gtArgs.AreArgsComplete()); + + CallArg* const methodHandleArg = lookupCall->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methodHandleArg != nullptr); + assert(methodHandleArg->GetEarlyNode()->TypeIs(TYP_I_IMPL)); + + // Spill the exact instantiated MethodDesc so the profile probe and dispatch lookup + // consume the same value without evaluating the method handle expression twice. + unsigned const methodTmpNum = + compiler->lvaGrabTemp(true DEBUGARG("generic virtual method profile tmp")); + compiler->lvaTable[methodTmpNum].lvType = TYP_I_IMPL; + + GenTree* const storeNode = + compiler->gtNewStoreLclVarNode(methodTmpNum, methodHandleArg->GetEarlyNode()); + GenTree* const methodHandleNode = compiler->gtNewLclvNode(methodTmpNum, TYP_I_IMPL); + GenTree* const methodProfileNode = compiler->gtNewIconNode((ssize_t)methodHistogram, TYP_I_IMPL); + GenTree* const profileCallNode = + compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_GENERICVIRTUALPROFILE32 + : CORINFO_HELP_GENERICVIRTUALPROFILE64, + TYP_VOID, methodHandleNode, methodProfileNode); + GenTree* const methodTmpNode = compiler->gtNewLclvNode(methodTmpNum, TYP_I_IMPL); + GenTree* const profileCommaNode = + compiler->gtNewOperNode(GT_COMMA, TYP_I_IMPL, profileCallNode, methodTmpNode); + GenTree* const storeCommaNode = + compiler->gtNewOperNode(GT_COMMA, TYP_I_IMPL, storeNode, profileCommaNode); + + methodHandleArg->SetEarlyNode(storeCommaNode); } else { - assert(call->IsVirtualVtable()); - GenTree* const baseMethodNode = compiler->gtNewIconEmbMethHndNode(call->gtCallMethHnd); - methodProfileCallNode = - compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_VTABLEPROFILE32 : CORINFO_HELP_VTABLEPROFILE64, - TYP_VOID, tmpNode, baseMethodNode, methodProfileNode); - } + GenTree* const tmpNode = compiler->gtNewLclvNode(tmpNum, TYP_REF); + GenTree* const methodProfileNode = compiler->gtNewIconNode((ssize_t)methodHistogram, TYP_I_IMPL); - if (helperCallNode == nullptr) - { - helperCallNode = methodProfileCallNode; - } - else - { - helperCallNode = compiler->gtNewOperNode(GT_COMMA, TYP_REF, helperCallNode, methodProfileCallNode); + GenTree* methodProfileCallNode; + if (call->IsDelegateInvoke()) + { + methodProfileCallNode = compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_DELEGATEPROFILE32 + : CORINFO_HELP_DELEGATEPROFILE64, + TYP_VOID, tmpNode, methodProfileNode); + } + else + { + assert(call->IsVirtualVtable()); + GenTree* const baseMethodNode = compiler->gtNewIconEmbMethHndNode(call->gtCallMethHnd); + methodProfileCallNode = + compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_VTABLEPROFILE32 : CORINFO_HELP_VTABLEPROFILE64, + TYP_VOID, tmpNode, baseMethodNode, methodProfileNode); + } + + if (helperCallNode == nullptr) + { + helperCallNode = methodProfileCallNode; + } + else + { + helperCallNode = compiler->gtNewOperNode(GT_COMMA, TYP_REF, helperCallNode, methodProfileCallNode); + } } } @@ -2599,7 +2636,8 @@ PhaseStatus Compiler::fgPrepareToInstrumentMethod() const bool useClassProfiles = (JitConfig.JitClassProfiling() > 0); const bool useDelegateProfiles = (JitConfig.JitDelegateProfiling() > 0); const bool useVTableProfiles = (JitConfig.JitVTableProfiling() > 0); - if (!prejit && (useClassProfiles || useDelegateProfiles || useVTableProfiles)) + const bool useGvmProfiles = (JitConfig.JitGenericVirtualProfiling() > 0); + if (!prejit && (useClassProfiles || useDelegateProfiles || useVTableProfiles || useGvmProfiles)) { fgHistogramInstrumentor = new (this, CMK_Pgo) HandleHistogramProbeInstrumentor(this); } diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 19b001e5026bb2..1fff68f86bcd47 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8200,7 +8200,7 @@ void Compiler::pickGDV(GenTreeCall* call, const int maxLikelyClasses = MAX_GDV_TYPE_CHECKS; LikelyClassMethodRecord likelyClasses[maxLikelyClasses]; unsigned numberOfClasses = 0; - if (call->IsVirtualStub() || call->IsVirtualVtable() || call->IsHelperCall()) + if (call->IsVirtualStub() || call->IsVirtualVtable() || call->IsHelperCall() || call->IsGenericVirtual(this)) { numberOfClasses = getLikelyClasses(likelyClasses, maxLikelyClasses, pgoInfo.PgoSchema, pgoInfo.PgoSchemaCount, pgoInfo.PgoData, ilOffset); @@ -8217,7 +8217,7 @@ void Compiler::pickGDV(GenTreeCall* call, // impDevirtualizeCall and what happens in // GuardedDevirtualizationTransformer::CreateThen for method GDV. // - if (!IsAot() && (call->IsVirtualVtable() || call->IsDelegateInvoke())) + if (!IsAot() && (call->IsVirtualVtable() || call->IsDelegateInvoke() || call->IsGenericVirtual(this))) { assert(!call->IsHelperCall()); numberOfMethods = getLikelyMethods(likelyMethods, maxLikelyMethods, pgoInfo.PgoSchema, pgoInfo.PgoSchemaCount, @@ -8295,8 +8295,9 @@ void Compiler::pickGDV(GenTreeCall* call, if ((verbose || JitConfig.EnableExtraSuperPmiQueries()) && (numberOfMethods > 0)) { - assert(call->gtCallType == CT_USER_FUNC); - const char* baseMethName = eeGetMethodFullName(call->gtCallMethHnd); + assert(call->gtCallType == CT_USER_FUNC || call->IsGenericVirtual(this)); + const char* baseMethName = + call->IsGenericVirtual(this) ? "" : eeGetMethodFullName(call->gtCallMethHnd); JITDUMP("Likely methods for call [%06u] to method %s\n", dspTreeID(call), baseMethName); for (UINT32 i = 0; i < numberOfMethods; i++) @@ -8663,12 +8664,6 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, { JITDUMP("Considering guarded devirtualization at IL offset %u (0x%x)\n", ilOffset, ilOffset); - if (call->IsGenericVirtual(this)) - { - JITDUMP("Generic virtual methods are not supported by guarded devirtualization, sorry.\n"); - return; - } - bool hasPgoData = true; CORINFO_CLASS_HANDLE likelyClasses[MAX_GDV_TYPE_CHECKS] = {}; @@ -8802,6 +8797,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, { CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; + CORINFO_METHOD_HANDLE originalMethod = call->IsGenericVirtual(this) ? likelyMethod : baseMethod; unsigned likelihood = likelihoods[candidateId]; const CORINFO_LOOKUP* pInstParamLookup = nullptr; CORINFO_RESOLVED_TOKEN* pResolvedToken = nullptr; @@ -8828,7 +8824,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Figure out which method will be called. // - dvInfo.virtualMethod = baseMethod; + dvInfo.virtualMethod = originalMethod; dvInfo.objClass = likelyClass; dvInfo.context = originalContext; dvInfo.pResolvedTokenVirtualMethod = nullptr; @@ -8926,7 +8922,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // Add this as a potential candidate. // addGuardedDevirtualizationCandidate(call, likelyMethod, likelyClass, likelyContext, likelyMethodAttribs, - likelyClassAttribs, likelihood, pInstParamLookup, baseMethod, + likelyClassAttribs, likelihood, pInstParamLookup, originalMethod, pResolvedToken, pUnboxedResolvedToken); } } @@ -8953,7 +8949,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, // classAttr - attributes of the class // likelihood - odds that this class is the class seen at runtime // instParamLookup - lookup to use if the target signature requires an instantiation argument -// originalMethodHandle - method handle of base method (before devirt) +// originalMethodHandle - method handle of base method (before devirt), including the profiled GVM instantiation // pResolvedToken - resolved token for methodHandle, used to get R2R call info; nullptr when unavailable // pUnboxedResolvedToken - resolved token for the unboxed entry, paired with pResolvedToken // @@ -8970,7 +8966,7 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, CORINFO_RESOLVED_TOKEN* pUnboxedResolvedToken) { // This transformation only makes sense for delegate and virtual calls - assert(call->IsDelegateInvoke() || call->IsVirtual()); + assert(call->IsDelegateInvoke() || call->IsVirtual() || call->IsGenericVirtual(this)); // Only mark calls if the feature is enabled. const bool isEnabled = JitConfig.JitEnableGuardedDevirtualization() > 0; @@ -9291,7 +9287,7 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call, return; } - // Ignore indirect calls, unless they are indirect virtual stub calls with profile info. + // Ignore indirect calls, unless they are indirect virtual stub calls or generic virtual calls with profile info. // if (call->gtCallType == CT_INDIRECT) { @@ -9302,7 +9298,7 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call, } else { - assert(call->IsVirtualStub()); + assert(call->IsVirtualStub() || call->IsGenericVirtual(this)); } } @@ -10588,7 +10584,7 @@ Compiler::GDVProbeType Compiler::compClassifyGDVProbeType(GenTreeCall* call) bool createTypeHistogram = false; if (JitConfig.JitClassProfiling() > 0) { - createTypeHistogram = call->IsVirtualStub() || call->IsVirtualVtable(); + createTypeHistogram = call->IsVirtualStub() || call->IsVirtualVtable() || call->IsGenericVirtual(this); // Cast helpers may conditionally (depending on whether the class is // exact or not) have probes. For those helpers we do not use this @@ -10598,8 +10594,10 @@ Compiler::GDVProbeType Compiler::compClassifyGDVProbeType(GenTreeCall* call) (call->gtHandleHistogramProfileCandidateInfo != nullptr)); } - bool createMethodHistogram = ((JitConfig.JitDelegateProfiling() > 0) && call->IsDelegateInvoke()) || - ((JitConfig.JitVTableProfiling() > 0) && call->IsVirtualVtable()); + bool createMethodHistogram = + ((JitConfig.JitDelegateProfiling() > 0) && call->IsDelegateInvoke()) || + ((JitConfig.JitVTableProfiling() > 0) && call->IsVirtualVtable()) || + ((JitConfig.JitGenericVirtualProfiling() > 0) && createTypeHistogram && call->IsGenericVirtual(this)); if (createTypeHistogram && createMethodHistogram) { diff --git a/src/coreclr/jit/indirectcalltransformer.cpp b/src/coreclr/jit/indirectcalltransformer.cpp index b7974c64cdfc22..883822e58b9f14 100644 --- a/src/coreclr/jit/indirectcalltransformer.cpp +++ b/src/coreclr/jit/indirectcalltransformer.cpp @@ -221,8 +221,17 @@ class IndirectCallTransformer for (GenTree** use : m_origCall->UseEdges()) { GenTree* node = *use; - if (((node->gtFlags & GTF_ALL_EFFECT) != 0) || - (!m_compiler->impIsInvariant(node) && m_compiler->gtHasLocalsWithAddrOp(node))) + + if ((use == &m_origCall->gtControlExpr) && node->IsCall() && + node->AsCall()->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)) + { + CallArg* methodHandleArg = + node->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methodHandleArg != nullptr); + SpillUseToTemp(block, &methodHandleArg->EarlyNodeRef()); + } + else if (((node->gtFlags & GTF_ALL_EFFECT) != 0) || + (!m_compiler->impIsInvariant(node) && m_compiler->gtHasLocalsWithAddrOp(node))) { SpillUseToTemp(block, use); } @@ -710,6 +719,28 @@ class IndirectCallTransformer prevCheckBlock->SetCond(prevCheckCheckEdge, prevCheckThenEdge); } + InlineCandidateInfo* guardedInfo = m_origCall->GetGDVCandidateInfo(checkIdx); + GenTree* runtimeGvmMethod = nullptr; + if (m_origCall->IsGenericVirtual(m_compiler)) + { + GenTreeCall* lookupCall = m_origCall->gtControlExpr->AsCall(); + assert(lookupCall->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)); + + CallArg* methodHandleArg = lookupCall->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); + assert(methodHandleArg != nullptr); + + unsigned const methodHandleTemp = + m_compiler->lvaGrabTemp(true DEBUGARG("generic virtual method guard")); + m_compiler->lvaTable[methodHandleTemp].lvType = TYP_I_IMPL; + + // Capture the lookup's instantiated base method so the guard and fallback use the same value. + // + GenTree* store = m_compiler->gtNewStoreLclVarNode(methodHandleTemp, methodHandleArg->GetEarlyNode()); + GenTree* methodHandle = m_compiler->gtNewLclvNode(methodHandleTemp, TYP_I_IMPL); + methodHandleArg->SetEarlyNode(m_compiler->gtNewOperNode(GT_COMMA, TYP_I_IMPL, store, methodHandle)); + runtimeGvmMethod = m_compiler->gtNewLclvNode(methodHandleTemp, TYP_I_IMPL); + } + CallArg* thisArg = m_origCall->gtArgs.GetThisArg(); SplitCall(m_checkBlock, &thisArg->EarlyNodeRef()); @@ -734,8 +765,6 @@ class IndirectCallTransformer return; } - InlineCandidateInfo* guardedInfo = m_origCall->GetGDVCandidateInfo(checkIdx); - // Create comparison. On success we will jump to do the indirect call. GenTree* compare; if (guardedInfo->guardedClassHandle != NO_CLASS_HANDLE) @@ -747,6 +776,17 @@ class IndirectCallTransformer GenTree* targetMethodTable = m_compiler->gtNewIconEmbClsHndNode(clsHnd); compare = m_compiler->gtNewOperNode(GT_NE, TYP_INT, targetMethodTable, methodTable); + + if (runtimeGvmMethod != nullptr) + { + GenTree* targetGvmMethod = m_compiler->gtNewIconEmbMethHndNode(guardedInfo->originalMethodHandle); + GenTree* methodCompare = + m_compiler->gtNewOperNode(GT_NE, TYP_INT, targetGvmMethod, runtimeGvmMethod); + GenTree* combinedCompare = m_compiler->gtNewOperNode(GT_OR, TYP_INT, compare, methodCompare); + GenTree* zero = m_compiler->gtNewIconNode(0, TYP_INT); + compare = m_compiler->gtNewOperNode(GT_NE, TYP_INT, combinedCompare, zero); + } + m_compiler->Metrics.ClassGDV++; } else diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 1e95c26b1e31cf..6b240002c3e67b 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -802,11 +802,12 @@ RELEASE_CONFIG_INTEGER(JitProfileCasts, "JitProfileCasts", 1) // Profile castc RELEASE_CONFIG_INTEGER(JitConsumeProfileForCasts, "JitConsumeProfileForCasts", 1) // Consume profile data (if any) // for castclass/isinst -RELEASE_CONFIG_INTEGER(JitClassProfiling, "JitClassProfiling", 1) // Profile virtual and interface calls -RELEASE_CONFIG_INTEGER(JitDelegateProfiling, "JitDelegateProfiling", 1) // Profile resolved delegate call targets -RELEASE_CONFIG_INTEGER(JitVTableProfiling, "JitVTableProfiling", 0) // Profile resolved vtable call targets -RELEASE_CONFIG_INTEGER(JitEdgeProfiling, "JitEdgeProfiling", 1) // Profile edges instead of blocks -RELEASE_CONFIG_INTEGER(JitCollect64BitCounts, "JitCollect64BitCounts", 0) // Collect counts as 64-bit values. +RELEASE_CONFIG_INTEGER(JitClassProfiling, "JitClassProfiling", 1) // Profile virtual and interface calls +RELEASE_CONFIG_INTEGER(JitDelegateProfiling, "JitDelegateProfiling", 1) // Profile resolved delegate call targets +RELEASE_CONFIG_INTEGER(JitVTableProfiling, "JitVTableProfiling", 0) // Profile resolved vtable call targets +RELEASE_CONFIG_INTEGER(JitGenericVirtualProfiling, "JitGenericVirtualProfiling", 1) // Profile resolved generic virtual call targets +RELEASE_CONFIG_INTEGER(JitEdgeProfiling, "JitEdgeProfiling", 1) // Profile edges instead of blocks +RELEASE_CONFIG_INTEGER(JitCollect64BitCounts, "JitCollect64BitCounts", 0) // Collect counts as 64-bit values. CONFIG_INTEGER(JitInstrumentIfOptimizing, "JitInstrumentIfOptimizing", 0) // 1: Always add instrumentation if optimizing // and not prejitting diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 48b3fe040ca56b..0b9fcca7787a3b 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -1756,7 +1756,8 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call // add as a non-standard arg. } } - else if ((call->gtCallType == CT_INDIRECT) && !call->IsVirtualStub() && (call->gtCallCookie != nullptr)) + else if ((call->gtCallType == CT_INDIRECT) && !call->IsVirtualStub() && !call->IsGenericVirtual(comp) && + (call->gtCallCookie != nullptr)) { assert(!call->IsUnmanaged()); diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs b/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs index 68f7b52f6ec048..98c03478fb9c7c 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs @@ -275,6 +275,8 @@ which is the right helper to use to allocate an object of a given type. */ CORINFO_HELP_DELEGATEPROFILE64, // Update 64-bit method profile for a delegate call site CORINFO_HELP_VTABLEPROFILE32, // Update 32-bit method profile for a vtable call site CORINFO_HELP_VTABLEPROFILE64, // Update 64-bit method profile for a vtable call site + CORINFO_HELP_GENERICVIRTUALPROFILE32, // Update 32-bit method profile for a generic virtual call site + CORINFO_HELP_GENERICVIRTUALPROFILE64, // Update 64-bit method profile for a generic virtual call site CORINFO_HELP_COUNTPROFILE32, // Update 32-bit block or edge count profile CORINFO_HELP_COUNTPROFILE64, // Update 64-bit block or edge count profile CORINFO_HELP_VALUEPROFILE32, // Update 32-bit value profile diff --git a/src/coreclr/vm/jithelpers.cpp b/src/coreclr/vm/jithelpers.cpp index 6330135b6f1c26..1ea70402b7905d 100644 --- a/src/coreclr/vm/jithelpers.cpp +++ b/src/coreclr/vm/jithelpers.cpp @@ -2081,6 +2081,62 @@ HCIMPL3(void, JIT_VTableProfile64, Object* obj, MethodDesc* pBaseMD, ICorJitInfo } HCIMPLEND +HCIMPL2(void, JIT_GenericVirtualProfile32, MethodDesc* pExactMD, ICorJitInfo::HandleHistogram32* methodProfile) +{ + FCALL_CONTRACT; + + size_t methodSampleIndex; + if (!CheckSample(&methodProfile->Count, &methodSampleIndex)) + { + return; + } + + _ASSERTE(pExactMD->IsVirtual()); + _ASSERTE(pExactMD->HasMethodInstantiation()); + + MethodDesc* pRecordedMD = (MethodDesc*)DEFAULT_UNKNOWN_HANDLE; + if (!pExactMD->GetLoaderAllocator()->IsCollectible() && !pExactMD->IsDynamicMethod()) + { + pRecordedMD = pExactMD; + } + +#ifdef _DEBUG + PgoManager::VerifyAddress(methodProfile); + PgoManager::VerifyAddress(methodProfile + 1); +#endif + + methodProfile->HandleTable[methodSampleIndex] = (CORINFO_METHOD_HANDLE)pRecordedMD; +} +HCIMPLEND + +HCIMPL2(void, JIT_GenericVirtualProfile64, MethodDesc* pExactMD, ICorJitInfo::HandleHistogram64* methodProfile) +{ + FCALL_CONTRACT; + + size_t methodSampleIndex; + if (!CheckSample(&methodProfile->Count, &methodSampleIndex)) + { + return; + } + + _ASSERTE(pExactMD->IsVirtual()); + _ASSERTE(pExactMD->HasMethodInstantiation()); + + MethodDesc* pRecordedMD = (MethodDesc*)DEFAULT_UNKNOWN_HANDLE; + if (!pExactMD->GetLoaderAllocator()->IsCollectible() && !pExactMD->IsDynamicMethod()) + { + pRecordedMD = pExactMD; + } + +#ifdef _DEBUG + PgoManager::VerifyAddress(methodProfile); + PgoManager::VerifyAddress(methodProfile + 1); +#endif + + methodProfile->HandleTable[methodSampleIndex] = (CORINFO_METHOD_HANDLE)pRecordedMD; +} +HCIMPLEND + // Helpers for scalable approximate counters // // Here threshold = 13 means we count accurately up to 2^13 = 8192 and From 87f604b9e727930f6f3b2451ce7fe18d9966df58 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 02:59:11 +0900 Subject: [PATCH 2/6] Fix format --- src/coreclr/jit/fgprofile.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/fgprofile.cpp b/src/coreclr/jit/fgprofile.cpp index 68f264dad01937..3642f932e18f7e 100644 --- a/src/coreclr/jit/fgprofile.cpp +++ b/src/coreclr/jit/fgprofile.cpp @@ -2166,7 +2166,8 @@ class HandleHistogramProbeInserter assert(call->IsVirtualVtable()); GenTree* const baseMethodNode = compiler->gtNewIconEmbMethHndNode(call->gtCallMethHnd); methodProfileCallNode = - compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_VTABLEPROFILE32 : CORINFO_HELP_VTABLEPROFILE64, + compiler->gtNewHelperCallNode(is32 ? CORINFO_HELP_VTABLEPROFILE32 + : CORINFO_HELP_VTABLEPROFILE64, TYP_VOID, tmpNode, baseMethodNode, methodProfileNode); } From 7390c7d4911c14f71759dd525eaacb57f84934a7 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 03:16:54 +0900 Subject: [PATCH 3/6] Format config as well --- src/coreclr/jit/jitconfigvalues.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 6b240002c3e67b..a078347bc94bea 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -802,12 +802,14 @@ RELEASE_CONFIG_INTEGER(JitProfileCasts, "JitProfileCasts", 1) // Profile castc RELEASE_CONFIG_INTEGER(JitConsumeProfileForCasts, "JitConsumeProfileForCasts", 1) // Consume profile data (if any) // for castclass/isinst -RELEASE_CONFIG_INTEGER(JitClassProfiling, "JitClassProfiling", 1) // Profile virtual and interface calls -RELEASE_CONFIG_INTEGER(JitDelegateProfiling, "JitDelegateProfiling", 1) // Profile resolved delegate call targets -RELEASE_CONFIG_INTEGER(JitVTableProfiling, "JitVTableProfiling", 0) // Profile resolved vtable call targets -RELEASE_CONFIG_INTEGER(JitGenericVirtualProfiling, "JitGenericVirtualProfiling", 1) // Profile resolved generic virtual call targets -RELEASE_CONFIG_INTEGER(JitEdgeProfiling, "JitEdgeProfiling", 1) // Profile edges instead of blocks -RELEASE_CONFIG_INTEGER(JitCollect64BitCounts, "JitCollect64BitCounts", 0) // Collect counts as 64-bit values. +RELEASE_CONFIG_INTEGER(JitClassProfiling, "JitClassProfiling", 1) // Profile virtual and interface calls +RELEASE_CONFIG_INTEGER(JitDelegateProfiling, "JitDelegateProfiling", 1) // Profile resolved delegate call targets +RELEASE_CONFIG_INTEGER(JitVTableProfiling, "JitVTableProfiling", 0) // Profile resolved vtable call targets +RELEASE_CONFIG_INTEGER(JitGenericVirtualProfiling, "JitGenericVirtualProfiling", 1) // Profile resolved generic virtual + // call targets + +RELEASE_CONFIG_INTEGER(JitEdgeProfiling, "JitEdgeProfiling", 1) // Profile edges instead of blocks +RELEASE_CONFIG_INTEGER(JitCollect64BitCounts, "JitCollect64BitCounts", 0) // Collect counts as 64-bit values. CONFIG_INTEGER(JitInstrumentIfOptimizing, "JitInstrumentIfOptimizing", 0) // 1: Always add instrumentation if optimizing // and not prejitting From b41c8aa90a8d716aeb84da7afdf14ac97f018ee2 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 18:20:08 +0900 Subject: [PATCH 4/6] Only pass the likely method when available --- src/coreclr/jit/importercalls.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 1fff68f86bcd47..23796377eb89a4 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -8797,12 +8797,14 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, { CORINFO_CLASS_HANDLE likelyClass = likelyClasses[candidateId]; CORINFO_METHOD_HANDLE likelyMethod = likelyMethods[candidateId]; - CORINFO_METHOD_HANDLE originalMethod = call->IsGenericVirtual(this) ? likelyMethod : baseMethod; unsigned likelihood = likelihoods[candidateId]; const CORINFO_LOOKUP* pInstParamLookup = nullptr; CORINFO_RESOLVED_TOKEN* pResolvedToken = nullptr; CORINFO_RESOLVED_TOKEN* pUnboxedResolvedToken = nullptr; + CORINFO_METHOD_HANDLE originalMethod = + call->IsGenericVirtual(this) && (likelyMethod != NO_METHOD_HANDLE) ? likelyMethod : baseMethod; + CORINFO_CONTEXT_HANDLE likelyContext = originalContext; CORINFO_DEVIRTUALIZATION_INFO dvInfo; From e88d1ad3bf438d7aacc0c75ddbecd8cd178ee962 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 19:34:54 +0900 Subject: [PATCH 5/6] NativeAOT support --- src/coreclr/jit/importercalls.cpp | 6 ++++-- src/coreclr/jit/indirectcalltransformer.cpp | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 23796377eb89a4..53b39f5177dc90 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1640,7 +1640,8 @@ var_types Compiler::impImportCall(OPCODE opcode, assert(call == origCall); assert(opts.OptEnabled(CLFLG_INLINING)); - assert(!isFatPointerCandidate); // We should not try to inline calli. + // Fat pointer calls can only reach this path as GDV candidates. + assert(!isFatPointerCandidate || isGuardedDevirtualizationCandidate); // Make the call its own tree (spill the stack if needed). // Do not consume the debug info here. This is particularly @@ -8992,7 +8993,8 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, // // If transforming these provides a benefit, we could save this off in the same way // we save the stub address below. - if ((call->gtCallType == CT_INDIRECT) && (call->AsCall()->gtCallCookie != nullptr)) + if ((call->gtCallType == CT_INDIRECT) && !call->IsGuardedDevirtualizationCandidate() && + (call->AsCall()->gtCallCookie != nullptr)) { JITDUMP("NOT Marking call [%06u] as guarded devirtualization candidate -- CT_INDIRECT with cookie\n", dspTreeID(call)); diff --git a/src/coreclr/jit/indirectcalltransformer.cpp b/src/coreclr/jit/indirectcalltransformer.cpp index 883822e58b9f14..47ae95585d6142 100644 --- a/src/coreclr/jit/indirectcalltransformer.cpp +++ b/src/coreclr/jit/indirectcalltransformer.cpp @@ -103,16 +103,17 @@ class IndirectCallTransformer for (Statement* const stmt : block->Statements()) { - if (m_compiler->doesMethodHaveFatPointer() && ContainsFatCalli(stmt)) + // NativeAOT GVM calls may require both transformations. Expand GDV first so that + // fat pointer handling is only applied to the residual indirect call. + if (m_compiler->doesMethodHaveGuardedDevirtualization() && ContainsGuardedDevirtualizationCandidate(stmt)) { - FatPointerCallTransformer transformer(m_compiler, block, stmt); + GuardedDevirtualizationTransformer transformer(m_compiler, block, stmt); transformer.Run(); count++; } - else if (m_compiler->doesMethodHaveGuardedDevirtualization() && - ContainsGuardedDevirtualizationCandidate(stmt)) + else if (m_compiler->doesMethodHaveFatPointer() && ContainsFatCalli(stmt)) { - GuardedDevirtualizationTransformer transformer(m_compiler, block, stmt); + FatPointerCallTransformer transformer(m_compiler, block, stmt); transformer.Run(); count++; } @@ -223,7 +224,8 @@ class IndirectCallTransformer GenTree* node = *use; if ((use == &m_origCall->gtControlExpr) && node->IsCall() && - node->AsCall()->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)) + (node->AsCall()->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR) || + node->AsCall()->IsHelperCall(CORINFO_HELP_GVMLOOKUP_FOR_SLOT))) { CallArg* methodHandleArg = node->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); @@ -721,10 +723,10 @@ class IndirectCallTransformer InlineCandidateInfo* guardedInfo = m_origCall->GetGDVCandidateInfo(checkIdx); GenTree* runtimeGvmMethod = nullptr; - if (m_origCall->IsGenericVirtual(m_compiler)) + if (m_origCall->IsGenericVirtual(m_compiler) && + m_origCall->gtControlExpr->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)) { GenTreeCall* lookupCall = m_origCall->gtControlExpr->AsCall(); - assert(lookupCall->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR)); CallArg* methodHandleArg = lookupCall->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle); assert(methodHandleArg != nullptr); From 67c54aecf3264d14253626d22795f08097a35fe8 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 16 Aug 2026 20:13:26 +0900 Subject: [PATCH 6/6] Nit --- src/coreclr/jit/indirectcalltransformer.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/jit/indirectcalltransformer.cpp b/src/coreclr/jit/indirectcalltransformer.cpp index 47ae95585d6142..85461062b7b66f 100644 --- a/src/coreclr/jit/indirectcalltransformer.cpp +++ b/src/coreclr/jit/indirectcalltransformer.cpp @@ -223,9 +223,7 @@ class IndirectCallTransformer { GenTree* node = *use; - if ((use == &m_origCall->gtControlExpr) && node->IsCall() && - (node->AsCall()->IsHelperCall(CORINFO_HELP_VIRTUAL_FUNC_PTR) || - node->AsCall()->IsHelperCall(CORINFO_HELP_GVMLOOKUP_FOR_SLOT))) + if (m_origCall->IsGenericVirtual(m_compiler) && (use == &m_origCall->gtControlExpr)) { CallArg* methodHandleArg = node->AsCall()->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle);