Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -1587,7 +1587,7 @@ struct CORINFO_DEVIRTUALIZATION_INFO
CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedMethod;
CORINFO_RESOLVED_TOKEN resolvedTokenDevirtualizedUnboxedMethod;
bool isInstantiatingStub;
bool wasArrayInterfaceDevirt;
bool needsMethodContext;
};

//----------------------------------------------------------------------------
Expand Down
10 changes: 7 additions & 3 deletions src/coreclr/jit/fginline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -612,9 +612,13 @@ class SubstitutePlaceholdersAndDevirtualizeWalker : public GenTreeVisitor<Substi

if (tree->OperIs(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);
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.
tryLateDevirt = call->IsGenericVirtual(m_compiler);
}

#ifdef DEBUG
tryLateDevirt = tryLateDevirt && (JitConfig.JitEnableLateDevirtualization() == 1);
Expand Down
5 changes: 2 additions & 3 deletions src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2374,9 +2374,8 @@ 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)));
// TODO: Support devirtualization for AOT generic virtual calls.
return IsVirtual() || (!compiler->IsAot() && IsGenericVirtual(compiler));
}

//-------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/jit/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
108 changes: 73 additions & 35 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -448,10 +457,11 @@ 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);
goto DONE;

goto DEVIRT;
}

case CORINFO_CALL:
Expand Down Expand Up @@ -968,6 +978,8 @@ var_types Compiler::impImportCall(OPCODE opcode,
}
}

DEVIRT:

bool probing;
probing = impConsiderCallProbe(call->AsCall(), rawILOffset);

Expand Down Expand Up @@ -7603,7 +7615,7 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call,
}

addGuardedDevirtualizationCandidate(call, exactMethod, exactCls, exactContext, exactMethodAttrs,
clsAttrs, likelyHood, dvInfo.wasArrayInterfaceDevirt,
clsAttrs, likelyHood, dvInfo.needsMethodContext,
dvInfo.isInstantiatingStub, baseMethod, originalContext);
}

Expand All @@ -7627,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 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;

Expand Down Expand Up @@ -7674,10 +7686,10 @@ void Compiler::considerGuardedDevirtualization(GenTreeCall* call,
break;
}

likelyContext = dvInfo.exactContext;
likelyMethod = dvInfo.devirtualizedMethod;
arrayInterface = dvInfo.wasArrayInterfaceDevirt;
instantiatingStub = dvInfo.isInstantiatingStub;
likelyContext = dvInfo.exactContext;
likelyMethod = dvInfo.devirtualizedMethod;
needsMethodContext = dvInfo.needsMethodContext;
instantiatingStub = dvInfo.isInstantiatingStub;
}
else
{
Expand Down Expand Up @@ -7752,7 +7764,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);
}
}
Expand All @@ -7778,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
// 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
Expand All @@ -7790,7 +7802,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)
Expand Down Expand Up @@ -7869,7 +7881,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
//
Expand Down Expand Up @@ -8603,6 +8615,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;
Expand Down Expand Up @@ -8643,7 +8661,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
// In non-R2R modes CALLVIRT <nonvirtual> 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());
Expand Down Expand Up @@ -8737,7 +8755,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");
}

Expand Down Expand Up @@ -8767,14 +8785,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);
}
Expand All @@ -8795,17 +8813,19 @@ 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.wasArrayInterfaceDevirt);
assert(dvInfo.needsMethodContext);

// We don't expect NAOT to end up here, since it has Array<T>
// 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());

Expand All @@ -8818,12 +8838,13 @@ 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 exact 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;
}

Expand Down Expand Up @@ -8920,25 +8941,42 @@ 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.
//
// 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.
CallArg* const methHndArg = call->gtArgs.FindWellKnownArg(WellKnownArg::RuntimeMethodHandle);
GenTree* const methHndNode = methHndArg != nullptr ? methHndArg->GetEarlyNode() : nullptr;
if (methHndNode && methHndNode->OperIs(GT_RUNTIMELOOKUP))
{
instParam = methHndNode;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain this behavior?

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

This comment was marked as outdated.

This comment was marked as outdated.

Copy link
Contributor Author

@hez2010 hez2010 Nov 30, 2025

Choose a reason for hiding this comment

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

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

}
// For cases where we don't have a RUNTIMELOOKUP helper call, we pass the instantiating stub.
if (instParam == nullptr)
{
instParam = gtNewIconEmbMethHndNode(instantiatingStub);
}
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)
Expand Down Expand Up @@ -9798,7 +9836,7 @@ void Compiler::impCheckCanInline(GenTreeCall* call,
pInfo->originalMethodHandle = nullptr;
pInfo->originalContextHandle = nullptr;
pInfo->likelihood = 0;
pInfo->arrayInterface = false;
pInfo->needsMethodContext = false;
}

pInfo->methInfo = methInfo;
Expand Down
5 changes: 2 additions & 3 deletions src/coreclr/jit/indirectcalltransformer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/inline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/jit/jitconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
RELEASE_CONFIG_INTEGER(JitEnableGenericVirtualDevirtualization, "JitEnableGenericVirtualDevirtualization", 1)

//
// MinOpts
//
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 3 additions & 3 deletions src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; } }
}

//----------------------------------------------------------------------------
Expand Down
Loading
Loading