-
Notifications
You must be signed in to change notification settings - Fork 5.3k
JIT: Devirtualize shared generic virtual methods #123323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
789055d
555ba83
25c0967
65a61bb
8d74fdb
c8b7f69
0aea180
5fbd527
a3d71c0
eb692c5
abd5db2
2eff218
2178332
d4c9f91
e96d509
21623ad
bc7d210
aeca172
aa460d5
263a6ae
eaf4493
264058e
19da409
9c5faa7
ecdf97f
2fb9e79
53a52d4
264dc62
d994cb1
3994537
014fb46
fc995e5
583277b
1aea555
d5e3485
267e187
ca8f876
757043d
2906173
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1305,6 +1305,7 @@ var_types Compiler::impImportCall(OPCODE opcode, | |||||||||||||||
| info->methodHnd = callInfo->hMethod; | ||||||||||||||||
| info->exactContextHnd = exactContextHnd; | ||||||||||||||||
| info->inlinersContext = compInlineContext; | ||||||||||||||||
| info->resolvedToken = *pResolvedToken; | ||||||||||||||||
| call->AsCall()->gtLateDevirtualizationInfo = info; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -7510,6 +7511,29 @@ bool Compiler::isCompatibleMethodGDV(GenTreeCall* call, CORINFO_METHOD_HANDLE gd | |||||||||||||||
| return true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| //------------------------------------------------------------------------ | ||||||||||||||||
| // impDevirtualizedCallHasConstInstParam: check if the instantiation argument | ||||||||||||||||
| // is a compile-time lookup. | ||||||||||||||||
| // | ||||||||||||||||
| // Arguments: | ||||||||||||||||
| // dvInfo - Devirtualization information returned by resolveVirtualMethod. | ||||||||||||||||
| // | ||||||||||||||||
| // Returns: | ||||||||||||||||
| // true if instParamLookup describes a valid constant handle/address lookup. | ||||||||||||||||
| // | ||||||||||||||||
| bool Compiler::impDevirtualizedCallHasConstInstParam(const CORINFO_DEVIRTUALIZATION_INFO& dvInfo) | ||||||||||||||||
| { | ||||||||||||||||
| if (dvInfo.instParamLookup.lookupKind.needsRuntimeLookup) | ||||||||||||||||
| { | ||||||||||||||||
| return false; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return ((dvInfo.instParamLookup.constLookup.accessType == IAT_VALUE && | ||||||||||||||||
| dvInfo.instParamLookup.constLookup.handle != nullptr) || | ||||||||||||||||
| (dvInfo.instParamLookup.constLookup.accessType == IAT_PVALUE && | ||||||||||||||||
| dvInfo.instParamLookup.constLookup.addr != nullptr)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| //------------------------------------------------------------------------ | ||||||||||||||||
| // considerGuardedDevirtualization: see if we can profitably guess at the | ||||||||||||||||
| // class involved in an interface or virtual call. | ||||||||||||||||
|
|
@@ -7629,6 +7653,8 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, | |||||||||||||||
| CORINFO_CONTEXT_HANDLE exactContext = dvInfo.exactContext; | ||||||||||||||||
| CORINFO_METHOD_HANDLE exactMethod = dvInfo.devirtualizedMethod; | ||||||||||||||||
| uint32_t exactMethodAttrs = info.compCompHnd->getMethodAttribs(exactMethod); | ||||||||||||||||
| const bool exactNeedsMethodContext = | ||||||||||||||||
| ((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD; | ||||||||||||||||
|
|
||||||||||||||||
| // NOTE: This is currently used only with NativeAOT. In theory, we could also check if we | ||||||||||||||||
| // have static PGO data to decide which class to guess first. Presumably, this is a rare case. | ||||||||||||||||
|
|
@@ -7643,8 +7669,9 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| addGuardedDevirtualizationCandidate(call, exactMethod, exactCls, exactContext, exactMethodAttrs, | ||||||||||||||||
| clsAttrs, likelyHood, dvInfo.needsMethodContext, | ||||||||||||||||
| dvInfo.isInstantiatingStub, baseMethod, originalContext); | ||||||||||||||||
| clsAttrs, likelyHood, exactNeedsMethodContext, | ||||||||||||||||
| impDevirtualizedCallHasConstInstParam(dvInfo), baseMethod, | ||||||||||||||||
| originalContext); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (call->GetInlineCandidatesCount() == numExactClasses) | ||||||||||||||||
|
|
@@ -7716,8 +7743,8 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call, | |||||||||||||||
|
|
||||||||||||||||
| likelyContext = dvInfo.exactContext; | ||||||||||||||||
| likelyMethod = dvInfo.devirtualizedMethod; | ||||||||||||||||
| needsMethodContext = dvInfo.needsMethodContext; | ||||||||||||||||
| instantiatingStub = dvInfo.isInstantiatingStub; | ||||||||||||||||
| needsMethodContext = ((size_t)likelyContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD; | ||||||||||||||||
| instantiatingStub = impDevirtualizedCallHasConstInstParam(dvInfo); | ||||||||||||||||
| } | ||||||||||||||||
| else | ||||||||||||||||
| { | ||||||||||||||||
|
|
@@ -8721,25 +8748,23 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, | |||||||||||||||
| const bool objClassIsFinal = (objClassAttribs & CORINFO_FLG_FINAL) != 0; | ||||||||||||||||
|
|
||||||||||||||||
| #if defined(DEBUG) | ||||||||||||||||
| const char* callKind = isInterface ? "interface" : "virtual"; | ||||||||||||||||
| const char* objClassNote = "[?]"; | ||||||||||||||||
| const char* objClassName = "?objClass"; | ||||||||||||||||
| const char* baseClassName = "?baseClass"; | ||||||||||||||||
| const char* baseMethodName = "?baseMethod"; | ||||||||||||||||
| const char* callKind = isInterface ? "interface" : "virtual"; | ||||||||||||||||
| const char* objClassNote = "[?]"; | ||||||||||||||||
| const char* objClassName = "?objClass"; | ||||||||||||||||
| const char* baseMethodFullName = "?baseMethod"; | ||||||||||||||||
|
|
||||||||||||||||
| if (verbose || doPrint) | ||||||||||||||||
| { | ||||||||||||||||
| objClassNote = isExact ? " [exact]" : objClassIsFinal ? " [final]" : ""; | ||||||||||||||||
| objClassName = eeGetClassName(objClass); | ||||||||||||||||
| baseClassName = eeGetClassName(baseClass); | ||||||||||||||||
| baseMethodName = eeGetMethodName(baseMethod); | ||||||||||||||||
| objClassNote = isExact ? " [exact]" : objClassIsFinal ? " [final]" : ""; | ||||||||||||||||
| objClassName = eeGetClassName(objClass); | ||||||||||||||||
| baseMethodFullName = eeGetMethodFullName(baseMethod); | ||||||||||||||||
|
|
||||||||||||||||
| if (verbose) | ||||||||||||||||
| { | ||||||||||||||||
| printf("\nimpDevirtualizeCall: Trying to devirtualize %s call:\n" | ||||||||||||||||
| " class for 'this' is %s%s (attrib %08x)\n" | ||||||||||||||||
| " base method is %s::%s\n", | ||||||||||||||||
| callKind, objClassName, objClassNote, objClassAttribs, baseClassName, baseMethodName); | ||||||||||||||||
| " base method is %s\n", | ||||||||||||||||
| callKind, objClassName, objClassNote, objClassAttribs, baseMethodFullName); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| #endif // defined(DEBUG) | ||||||||||||||||
|
|
@@ -8788,26 +8813,29 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, | |||||||||||||||
|
|
||||||||||||||||
| info.compCompHnd->resolveVirtualMethod(&dvInfo); | ||||||||||||||||
|
|
||||||||||||||||
| CORINFO_METHOD_HANDLE derivedMethod = dvInfo.devirtualizedMethod; | ||||||||||||||||
| CORINFO_CONTEXT_HANDLE exactContext = dvInfo.exactContext; | ||||||||||||||||
| CORINFO_CLASS_HANDLE derivedClass = NO_CLASS_HANDLE; | ||||||||||||||||
| CORINFO_RESOLVED_TOKEN* pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedMethod; | ||||||||||||||||
| CORINFO_METHOD_HANDLE derivedMethod = dvInfo.devirtualizedMethod; | ||||||||||||||||
| CORINFO_CONTEXT_HANDLE exactContext = dvInfo.exactContext; | ||||||||||||||||
| CORINFO_CLASS_HANDLE derivedClass = NO_CLASS_HANDLE; | ||||||||||||||||
| CORINFO_RESOLVED_TOKEN* pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedMethod; | ||||||||||||||||
| const bool needsRuntimeLookup = dvInfo.instParamLookup.lookupKind.needsRuntimeLookup; | ||||||||||||||||
| const bool needsCompileTimeLookup = impDevirtualizedCallHasConstInstParam(dvInfo); | ||||||||||||||||
| const bool needsInstParam = needsRuntimeLookup || needsCompileTimeLookup; | ||||||||||||||||
|
|
||||||||||||||||
| if (derivedMethod != nullptr) | ||||||||||||||||
| { | ||||||||||||||||
| assert(exactContext != nullptr); | ||||||||||||||||
|
|
||||||||||||||||
| if (((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_CLASS) | ||||||||||||||||
| { | ||||||||||||||||
| assert(!dvInfo.needsMethodContext); | ||||||||||||||||
| assert(!needsInstParam); | ||||||||||||||||
| 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. | ||||||||||||||||
| // Generic virtual method devirt also returns a generic method. | ||||||||||||||||
| // | ||||||||||||||||
| assert(call->IsGenericVirtual(this) || dvInfo.needsMethodContext); | ||||||||||||||||
| assert(call->IsGenericVirtual(this) || needsInstParam || ((objClassAttribs & CORINFO_FLG_ARRAY) != 0)); | ||||||||||||||||
| assert(((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD); | ||||||||||||||||
| derivedClass = info.compCompHnd->getMethodClass(derivedMethod); | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -8818,54 +8846,30 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, | |||||||||||||||
| bool canDevirtualize = false; | ||||||||||||||||
|
|
||||||||||||||||
| #if defined(DEBUG) | ||||||||||||||||
| const char* derivedClassName = "?derivedClass"; | ||||||||||||||||
| const char* derivedMethodName = "?derivedMethod"; | ||||||||||||||||
| const char* note = "inexact or not final"; | ||||||||||||||||
| const char* instArg = ""; | ||||||||||||||||
| const char* derivedMethodFullName = "?derivedMethod"; | ||||||||||||||||
| const char* note = "inexact or not final"; | ||||||||||||||||
| const char* instArg = ""; | ||||||||||||||||
| #endif | ||||||||||||||||
|
|
||||||||||||||||
| CORINFO_METHOD_HANDLE instantiatingStub = NO_METHOD_HANDLE; | ||||||||||||||||
|
|
||||||||||||||||
| if (dvInfo.isInstantiatingStub) | ||||||||||||||||
| if (derivedMethod != nullptr && needsInstParam) | ||||||||||||||||
| { | ||||||||||||||||
| // We should only end up with generic methods that needs a method context (eg. array interface, GVM). | ||||||||||||||||
| // | ||||||||||||||||
| assert(dvInfo.needsMethodContext); | ||||||||||||||||
|
|
||||||||||||||||
| // We don't expect NAOT to end up here, since it has Array<T> | ||||||||||||||||
| // and normal devirtualization. | ||||||||||||||||
| // | ||||||||||||||||
| assert(!IsTargetAbi(CORINFO_NATIVEAOT_ABI)); | ||||||||||||||||
|
|
||||||||||||||||
| // We don't expect R2R to end up here, since it does not (yet) support | ||||||||||||||||
| // array interface devirtualization. | ||||||||||||||||
| // | ||||||||||||||||
| assert(!IsAot()); | ||||||||||||||||
|
|
||||||||||||||||
| // We don't expect there to be an existing inst param arg. | ||||||||||||||||
| // | ||||||||||||||||
| CallArg* const instParam = call->gtArgs.FindWellKnownArg(WellKnownArg::InstParam); | ||||||||||||||||
| if (instParam != nullptr) | ||||||||||||||||
| if (needsCompileTimeLookup) | ||||||||||||||||
| { | ||||||||||||||||
| assert(!"unexpected inst param in virtual/interface call"); | ||||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
| // We should only end up with generic methods that need a method context (eg. array interface, GVM). | ||||||||||||||||
| // | ||||||||||||||||
| assert(((size_t)exactContext & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_METHOD); | ||||||||||||||||
|
|
||||||||||||||||
| // If we don't know the array type exactly we may have the wrong interface type here. | ||||||||||||||||
| // Bail out. | ||||||||||||||||
| // | ||||||||||||||||
| if (!isExact) | ||||||||||||||||
| { | ||||||||||||||||
| JITDUMP("Array interface devirt: array type is inexact, sorry.\n"); | ||||||||||||||||
| return; | ||||||||||||||||
| // We don't expect R2R/NAOT to end up here for array interface devirtualization. | ||||||||||||||||
| // For NAOT, it has Array<T> and normal devirtualization. | ||||||||||||||||
| // For R2R, we don't (yet) support array interface devirtualization. | ||||||||||||||||
| assert(call->IsGenericVirtual(this) || !IsAot()); | ||||||||||||||||
|
|
||||||||||||||||
| instantiatingStub = (CORINFO_METHOD_HANDLE)((size_t)exactContext & ~CORINFO_CONTEXTFLAGS_MASK); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // We want to inline the instantiating stub. Fetch the relevant info. | ||||||||||||||||
| // | ||||||||||||||||
| CORINFO_CLASS_HANDLE ignored = NO_CLASS_HANDLE; | ||||||||||||||||
| derivedMethod = info.compCompHnd->getInstantiatedEntry(derivedMethod, &instantiatingStub, &ignored); | ||||||||||||||||
| assert(ignored == NO_CLASS_HANDLE); | ||||||||||||||||
| assert((derivedMethod == NO_METHOD_HANDLE) || (instantiatingStub != NO_METHOD_HANDLE)); | ||||||||||||||||
| assert(!needsCompileTimeLookup || (instantiatingStub != NO_METHOD_HANDLE)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // If we failed to get a method handle, we can't directly devirtualize. | ||||||||||||||||
|
|
@@ -8896,18 +8900,18 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, | |||||||||||||||
| { | ||||||||||||||||
| note = "final method"; | ||||||||||||||||
| } | ||||||||||||||||
| if (dvInfo.isInstantiatingStub) | ||||||||||||||||
| if (needsInstParam) | ||||||||||||||||
| { | ||||||||||||||||
| instArg = " [instantiating stub]"; | ||||||||||||||||
| instArg = needsCompileTimeLookup ? eeGetMethodFullName(instantiatingStub) : "runtime lookup"; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (verbose || doPrint) | ||||||||||||||||
| { | ||||||||||||||||
| derivedMethodName = eeGetMethodName(derivedMethod); | ||||||||||||||||
| derivedClassName = eeGetClassName(derivedClass); | ||||||||||||||||
| derivedMethodFullName = eeGetMethodFullName(derivedMethod); | ||||||||||||||||
| if (verbose) | ||||||||||||||||
| { | ||||||||||||||||
| printf(" devirt to %s::%s -- %s%s\n", derivedClassName, derivedMethodName, note, instArg); | ||||||||||||||||
| printf(" devirt to %s -- %s%s%s\n", derivedMethodFullName, note, | ||||||||||||||||
| needsInstParam ? ", instantiating stub: " : "", instArg); | ||||||||||||||||
| gtDispTree(call); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -8946,24 +8950,46 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call, | |||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Insert the instantiation argument when necessary. | ||||||||||||||||
| if (needsInstParam) | ||||||||||||||||
| { | ||||||||||||||||
| assert(call->gtArgs.FindWellKnownArg(WellKnownArg::InstParam) == nullptr); | ||||||||||||||||
|
|
||||||||||||||||
| CORINFO_METHOD_HANDLE compileTimeHandle = derivedMethod; | ||||||||||||||||
| if (needsCompileTimeLookup) | ||||||||||||||||
| { | ||||||||||||||||
| compileTimeHandle = instantiatingStub; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| CORINFO_RESOLVED_TOKEN* lookupToken = | ||||||||||||||||
| (pDerivedResolvedToken->tokenScope != nullptr) ? pDerivedResolvedToken : pResolvedToken; | ||||||||||||||||
|
||||||||||||||||
| (pDerivedResolvedToken->tokenScope != nullptr) ? pDerivedResolvedToken : pResolvedToken; | |
| (pDerivedResolvedToken->tokenScope != nullptr) ? pDerivedResolvedToken : pResolvedToken; | |
| if (lookupToken == nullptr) | |
| { | |
| JITDUMP("Failed to materialize instantiation argument for devirtualized call: no resolved token.\n"); | |
| return; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GVMs are not being profiled yet so they won't hit the GDV code path.
hez2010 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR enables devirtualization of shared generic virtual method targets by introducing instParamLookup and new runtime lookup kinds. There are many existing JIT tests for generic virtual methods/devirtualization, but none updated here to cover the new shared-target + instantiation-arg lookup behavior (including late devirtualization). Please add/extend a JIT regression test that exercises this scenario (ideally with late devirt enabled) to guard against future regressions in instParamLookup materialization and the new DevirtualizedMethodDescSlot runtime lookup encoding.
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR introduces new behavior for shared GVM devirtualization (including new instantiation-parameter runtime lookup handling). Please add a focused JIT regression test (e.g., under src/tests/JIT) that exercises both interface and virtual-call cases where the devirtualized target is a shared generic method requiring an instantiation argument, to prevent future regressions (especially for late devirtualization / post-import phases).
Uh oh!
There was an error while loading. Please reload this page.