Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
61a278f
Devirtualize virtual methods that require an instantiating stub
hez2010 May 28, 2026
f01f4f4
R2R support
hez2010 May 28, 2026
b42c291
Bail shared MT for generic DIM as well
hez2010 May 28, 2026
b96fc2e
Stop returning an instantiating stub
hez2010 May 28, 2026
de840c2
Nit
hez2010 May 28, 2026
181e88e
Nit 2
hez2010 May 28, 2026
9331cc1
Remove redundant empty line
hez2010 May 28, 2026
f635686
Generic DIM devirt for NativeAOT
hez2010 May 28, 2026
5da42c5
Meh
hez2010 May 28, 2026
66c8d6a
More NativeAOT support
hez2010 May 28, 2026
152c44a
Address an assertion
hez2010 May 28, 2026
466ba2c
Use IsSharedByGenericMethodInstantiations
hez2010 May 28, 2026
dca1834
Add a couple of tests
hez2010 May 28, 2026
2582ba6
Address test name collisions
hez2010 May 29, 2026
800aa20
Fix crossgen2 not producing a const lookup
hez2010 May 30, 2026
a5c4574
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 May 30, 2026
31ff006
Fix NativeAOT build
hez2010 May 30, 2026
645e5dd
Pass a type dictionary fixup when the method doesn't have an instanti…
hez2010 May 30, 2026
1f2aacc
Nit
hez2010 May 30, 2026
cad1140
More fixes to AOT
hez2010 May 30, 2026
0c7102a
Use a better helper
hez2010 May 30, 2026
8f02750
Minor refactor
hez2010 May 30, 2026
e27aba9
Handle unboxing stub
hez2010 May 30, 2026
b77ad2d
Bail out unboxing stub in R2R for now
hez2010 May 30, 2026
de3bd27
Clean up tests
hez2010 May 31, 2026
60dfd3f
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 Jun 1, 2026
41e0827
Nit
hez2010 Jun 1, 2026
b55d5c1
Take canonical subtype into account as well
hez2010 Jun 1, 2026
0820190
Add a test case
hez2010 Jun 1, 2026
d47b382
Refactor and properly handle unboxing stubs
hez2010 Jun 2, 2026
fe25211
Cleanup getUnboxedEntry and getInstantiatedEntry
hez2010 Jun 11, 2026
852a572
Check the context directly
hez2010 Jun 11, 2026
959ad95
Merge branch 'main' into unboxing-stub-cleanup
hez2010 Jun 13, 2026
872c5dc
Merge branch 'main' into unboxing-stub-cleanup
hez2010 Jun 14, 2026
66f0836
Merge branch 'main' into unboxing-stub-cleanup
hez2010 Jun 19, 2026
272667a
Merge branch 'main' into unboxing-stub-cleanup
hez2010 Jun 20, 2026
4b0568e
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 Jun 20, 2026
109a293
Merge branch 'unboxing-stub-cleanup' into devirt-instantiating-stub-c…
hez2010 Jun 20, 2026
fe5b4fa
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 Jun 21, 2026
176d6b9
Rework unboxing stub and instantiating stub
hez2010 Jun 20, 2026
5dec373
Address some verification issues
hez2010 Jun 21, 2026
d0b8e36
Add file header
hez2010 Jun 21, 2026
e0bbb26
Reduce generic nesting depth to make ilc compile
hez2010 Jun 22, 2026
231533d
Gate the unboxing stub test case for NativeAOT
hez2010 Jun 22, 2026
f7145a5
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 Jun 22, 2026
933f030
Merge branch 'main' into devirt-instantiating-stub-coreclr
hez2010 Jun 24, 2026
044a8c3
Revert "Gate the unboxing stub test case for NativeAOT"
hez2010 Jun 24, 2026
374e5e3
Refine tests
hez2010 Jun 24, 2026
862d314
Exercise s_list element as well
hez2010 Jun 24, 2026
9f681d5
Oops
hez2010 Jun 24, 2026
07d6acd
More coverage
hez2010 Jun 24, 2026
66a508e
Expand to full test matrix
hez2010 Jun 24, 2026
bf8e205
Meh
hez2010 Jun 24, 2026
b67dcac
CoreCLR support
hez2010 Jun 1, 2026
e81ac20
R2R
hez2010 Jun 1, 2026
42858f8
Remove redundant code after merging
hez2010 Jun 24, 2026
ff93779
Cleanup
hez2010 Jun 24, 2026
527470d
Various fixes
hez2010 Jun 24, 2026
82d440c
Merge branch 'devirt-instantiating-stub-coreclr' into devirt-runtime-…
hez2010 Jun 24, 2026
b76ac1e
Address a missing case
hez2010 Jun 24, 2026
4517553
Clarify the instArg check for R2R
hez2010 Jun 24, 2026
ef125d3
Merge branch 'devirt-instantiating-stub-coreclr' into devirt-runtime-…
hez2010 Jun 24, 2026
66bf730
JIT format
hez2010 Jun 24, 2026
7461e66
Merge branch 'main' into devirt-runtime-lookups
hez2010 Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -8096,7 +8096,8 @@ class Compiler
bool isInterface,
CORINFO_METHOD_HANDLE baseMethod,
CORINFO_CLASS_HANDLE baseClass,
CORINFO_CONTEXT_HANDLE* pContextHandle);
CORINFO_CONTEXT_HANDLE* pContextHandle,
CORINFO_RESOLVED_TOKEN* pResolvedToken);

bool isCompatibleMethodGDV(GenTreeCall* call, CORINFO_METHOD_HANDLE gdvTarget);

Expand Down
15 changes: 8 additions & 7 deletions src/coreclr/jit/fginline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -608,16 +608,17 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitor<Substi
}
#endif // DEBUG

CORINFO_METHOD_HANDLE method = call->gtLateDevirtualizationInfo->methodHnd;
CORINFO_CONTEXT_HANDLE context = call->gtLateDevirtualizationInfo->exactContextHnd;
InlineContext* inlinersContext = call->gtInlineContext;
unsigned methodFlags = 0;
const bool isLateDevirtualization = true;
const bool explicitTailCall = call->IsTailPrefixedCall();
CORINFO_METHOD_HANDLE method = call->gtLateDevirtualizationInfo->methodHnd;
CORINFO_CONTEXT_HANDLE context = call->gtLateDevirtualizationInfo->exactContextHnd;
InlineContext* inlinersContext = call->gtInlineContext;
CORINFO_RESOLVED_TOKEN* pResolvedToken = &call->gtLateDevirtualizationInfo->resolvedToken;
unsigned methodFlags = 0;
const bool isLateDevirtualization = true;
const bool explicitTailCall = call->IsTailPrefixedCall();

CORINFO_CONTEXT_HANDLE contextInput = context;
context = nullptr;
m_compiler->impDevirtualizeCall(call, nullptr, &method, &methodFlags, &contextInput, &context,
m_compiler->impDevirtualizeCall(call, pResolvedToken, &method, &methodFlags, &contextInput, &context,
isLateDevirtualization, explicitTailCall);

if (!call->IsDevirtualizationCandidate(m_compiler))
Expand Down
17 changes: 11 additions & 6 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
else if (call->AsCall()->IsDelegateInvoke())
{
considerGuardedDevirtualization(call->AsCall(), rawILOffset, false, call->AsCall()->gtCallMethHnd,
NO_CLASS_HANDLE, nullptr);
NO_CLASS_HANDLE, nullptr, pResolvedToken);
}
}

Expand Down Expand Up @@ -1291,6 +1291,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
info->methodHnd = callInfo->hMethod;
info->exactContextHnd = exactContextHnd;
info->ilLocation = impCurStmtDI.GetLocation();
info->resolvedToken = *pResolvedToken;
call->AsCall()->gtLateDevirtualizationInfo = info;
}
}
Expand Down Expand Up @@ -7865,7 +7866,8 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call,
bool isInterface,
CORINFO_METHOD_HANDLE baseMethod,
CORINFO_CLASS_HANDLE baseClass,
CORINFO_CONTEXT_HANDLE* pContextHandle)
CORINFO_CONTEXT_HANDLE* pContextHandle,
CORINFO_RESOLVED_TOKEN* pResolvedToken)
{
JITDUMP("Considering guarded devirtualization at IL offset %u (0x%x)\n", ilOffset, ilOffset);

Expand Down Expand Up @@ -7946,7 +7948,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call,
dvInfo.virtualMethod = baseMethod;
dvInfo.objClass = exactCls;
dvInfo.context = originalContext;
dvInfo.pResolvedTokenVirtualMethod = nullptr;
dvInfo.pResolvedTokenVirtualMethod = pResolvedToken;

JITDUMP("GDV exact: resolveVirtualMethod (method %p class %p context %p)\n", dvInfo.virtualMethod,
dvInfo.objClass, dvInfo.context);
Expand Down Expand Up @@ -9088,7 +9090,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
return;
}

considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, baseClass, pContextHandle);
considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, baseClass, pContextHandle,
pResolvedToken);

return;
}
Expand Down Expand Up @@ -9132,7 +9135,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
return;
}

considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, baseClass, pContextHandle);
considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, baseClass, pContextHandle,
pResolvedToken);
return;
}

Expand Down Expand Up @@ -9254,7 +9258,8 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
return;
}

considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, objClass, pContextHandle);
considerGuardedDevirtualization(call, ilOffset, isInterface, baseMethod, objClass, pContextHandle,
pResolvedToken);
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/inline.h
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ struct LateDevirtualizationInfo
CORINFO_METHOD_HANDLE methodHnd;
CORINFO_CONTEXT_HANDLE exactContextHnd;
ILLocation ilLocation;
CORINFO_RESOLVED_TOKEN resolvedToken;
};

// InlArgInfo describes inline candidate argument properties.
Expand Down
19 changes: 6 additions & 13 deletions src/coreclr/tools/Common/Compiler/DevirtualizationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,19 @@ protected virtual MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefType
return null;

case DefaultInterfaceMethodResolution.DefaultImplementation:
if (dimMethod.OwningType.HasInstantiation || (declMethod != defaultInterfaceDispatchDeclMethod))
if (declMethod != defaultInterfaceDispatchDeclMethod)
{
// If we devirtualized into a default interface method on a generic type, we should actually return an
// instantiating stub but this is not happening.
// Making this work is tracked by https://github.com/dotnet/runtime/issues/9588

// In addition, we fail here for variant default interface dispatch
// Fail for variant default interface dispatch
devirtualizationDetail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_DIM;
return null;
}
else
{
impl = dimMethod;
if (originalDeclMethod.HasInstantiation)
{
impl = impl.GetMethodDefinition().MakeInstantiatedMethod(originalDeclMethod.Instantiation);
}
}
break;
}
Expand Down Expand Up @@ -219,13 +219,6 @@ protected virtual MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefType
}
}

if (impl != null && impl.HasInstantiation && impl.GetCanonMethodTarget(CanonicalFormKind.Specific).IsCanonicalMethod(CanonicalFormKind.Specific))
{
// We don't support devirtualization of shared generic virtual methods yet.
devirtualizationDetail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
impl = null;
}

return impl;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ private static void ForEachEmbeddedGenericFormalWorker(TypeDesc type, Instantiat
TypeDesc genericTypeDefinition = type.GetTypeDefinition();
Instantiation genericTypeParameters = genericTypeDefinition.Instantiation;
Instantiation genericTypeArguments = type.Instantiation;
for (int i = 0; i < genericTypeArguments.Length; i++)

// We have some negative IL tests that reference a generic type with more type arguments
// than the type definition has type parameters.
// Tolerate them as such a type can never load and therefore cannot form a cycle.
for (int i = 0; i < genericTypeArguments.Length && i < genericTypeParameters.Length; i++)
{
var genericTypeParameter = (EcmaGenericParameter)genericTypeParameters[i];
TypeDesc genericTypeArgument = genericTypeArguments[i];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum DictionaryEntryKind
DispatchStubAddrSlot = 5,
FieldDescSlot = 6,
DeclaringTypeHandleSlot = 7,
DevirtualizedMethodDescSlot = 8,
}

public enum ReadyToRunFixupKind
Expand Down
86 changes: 85 additions & 1 deletion src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,90 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info)
info->resolvedTokenDevirtualizedUnboxedMethod = default(CORINFO_RESOLVED_TOKEN);
}

bool isArray = decl.OwningType.IsInterface && objType.IsArray;
bool isGenericVirtual = !isArray && decl.HasInstantiation;
MethodDesc instArgTarget = unboxingStub ? nonUnboxingImpl : impl;
bool requiresInstMethodDescArg = instArgTarget.RequiresInstMethodDescArg();
bool requiresInstMethodTableArg = instArgTarget.RequiresInstMethodTableArg();

// For unboxing stubs whose unboxed entry needs a MethodTable inst arg, the boxed object supplies the exact MT.
// For MethodDesc cases we always need to supply the exact MD.
if (requiresInstMethodDescArg || (requiresInstMethodTableArg && !unboxingStub))
{
if (originalImpl.OwningType.IsCanonicalSubtype(CanonicalFormKind.Any))
{
// If we end up with a shared MethodTable that is not exact,
// we can't devirtualize since it's not possible to compute the instantiation argument even as a runtime lookup.
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
return false;
}

if (originalImpl.IsRuntimeDeterminedExactMethod || originalImpl.IsSharedByGenericInstantiations)
{
if (info->pResolvedTokenVirtualMethod == null || unboxingStub)
{
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
return false;
}

#if READYTORUN
ComputeRuntimeLookupForSharedGenericToken(
Internal.ReadyToRunConstants.DictionaryEntryKind.DevirtualizedMethodDescSlot,
ref *info->pResolvedTokenVirtualMethod,
null,
originalImpl,
MethodBeingCompiled,
ref info->instParamLookup);
#else
// TODO: Implement generic virtual method devirtualization runtime lookup for NativeAOT
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
return false;
#endif
}
}

if (isArray)
{
#if READYTORUN
// Array interface devirt is not yet supported by R2R.
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
return false;
#else
// NativeAOT handles arrays in a different way that doesn't require an instantiating stub.
#endif
}

if (!info->instParamLookup.lookupKind.needsRuntimeLookup)
{
if (requiresInstMethodDescArg)
{
if (unboxingStub)
{
// Bail out for now. We need an unboxing stub that points to an instantiated method.
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_CANON;
return false;
}
#if READYTORUN
MethodWithToken originalImplWithToken = new MethodWithToken(originalImpl, methodWithTokenImpl.Token, null, false, null, null);
info->instParamLookup.constLookup = CreateConstLookupToSymbol(_compilation.SymbolNodeFactory.CreateReadyToRunHelper(ReadyToRunHelperId.MethodDictionary, originalImplWithToken));
#else
info->instParamLookup.constLookup = CreateConstLookupToSymbol(_compilation.NodeFactory.MethodGenericDictionary(originalImpl));
#endif
}
else if (requiresInstMethodTableArg)
{
if (!unboxingStub)
{
#if READYTORUN
info->instParamLookup.constLookup = CreateConstLookupToSymbol(_compilation.SymbolNodeFactory.CreateReadyToRunHelper(ReadyToRunHelperId.TypeDictionary, originalImpl.OwningType));

#else
info->instParamLookup.constLookup = CreateConstLookupToSymbol(_compilation.NodeFactory.ConstructedTypeSymbol(originalImpl.OwningType));
#endif
}
}
}

#if READYTORUN
// Testing has not shown that concerns about virtual matching are significant
// Only generate verification for builds with the stress mode enabled
Expand All @@ -1497,7 +1581,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info)
#endif
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_SUCCESS;
info->devirtualizedMethod = ObjectToHandle(impl);
info->tokenLookupContext = contextFromType(owningType);
info->tokenLookupContext = (isArray || isGenericVirtual) ? contextFromMethod(originalImpl) : contextFromType(owningType);

return true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ protected override MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefTyp
//
// Result method checking
// 1. Ensure that the resolved result versions with the code, or is the decl method
// 2. Devirtualizing to a default interface method is not currently considered to be useful, and how to check for version
// resilience has not yet been analyzed.
// 2. When devirtualizing to a default interface method, the resolved result method must version with the code.
// 3. When checking that the resolved result versions with the code, validate that all of the types
// From implType to the owning type of resolved result method also version with the code.

Expand Down Expand Up @@ -163,6 +162,17 @@ protected override MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefTyp

if (resolvedVirtualMethod != null)
{
if (resolvedVirtualMethod.OwningType.IsInterface)
{
if (_compilationModuleGroup.VersionsWithMethodBody(resolvedVirtualMethod))
{
return resolvedVirtualMethod;
}

devirtualizationDetail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_BUBBLE;
return null;
}

// Validate that the inheritance chain for resolution is within version bubble
// The rule is somewhat tricky here.
// If the resolved method is the declMethod, then only types which derive from the
Expand Down
Loading
Loading