diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 4420a7662a36cc..a1e152f7161d87 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -3830,6 +3830,8 @@ class Compiler bool impCanPInvokeInlineCallSite(BasicBlock* block); void impCheckForPInvokeCall( GenTreeCall* call, CORINFO_METHOD_HANDLE methHnd, CORINFO_SIG_INFO* sig, unsigned mflags, BasicBlock* block); + + bool impCanSubstituteSig(CORINFO_SIG_INFO* sourceSig, CORINFO_SIG_INFO* targetSig, var_types sourceThis, var_types targetThis); GenTreeCall* impImportIndirectCall(CORINFO_SIG_INFO* sig, const DebugInfo& di = DebugInfo()); void impPopArgsForUnmanagedCall(GenTreeCall* call, CORINFO_SIG_INFO* sig); @@ -3858,6 +3860,7 @@ class Compiler GenTree* impInitClass(CORINFO_RESOLVED_TOKEN* pResolvedToken); + GenTree* impGetNodeFromLocal(GenTree* node); GenTree* impImportStaticReadOnlyField(CORINFO_FIELD_HANDLE field, CORINFO_CLASS_HANDLE ownerCls); GenTree* impImportCnsTreeFromBuffer(uint8_t* buffer, var_types valueType); diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index fca9e62a5825e3..c9a8dcca61aa9f 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -3764,6 +3764,65 @@ GenTree* Compiler::impInitClass(CORINFO_RESOLVED_TOKEN* pResolvedToken) return node; } +//------------------------------------------------------------------------ +// impGetNodeFromLocal: Tries to return the node that's assigned +// to the provided local. +// +// Arguments: +// node - GT_LCL_VAR whose value is searched for +// +// Return Value: +// The tree representing the node assigned to the variable when possible, +// nullptr otherwise. +// +GenTree* Compiler::impGetNodeFromLocal(GenTree* node) +{ + assert(node != nullptr); + assert(node->OperIs(GT_LCL_VAR)); + + unsigned lclNum = node->AsLclVarCommon()->GetLclNum(); + + if (lvaTable[lclNum].lvSingleDef == 0) + { + return nullptr; + } + + auto findValue = [&](Statement* stmtList) -> GenTree* { + for (Statement* stmt : StatementList(stmtList)) + { + GenTree* root = stmt->GetRootNode(); + if (root->OperIs(GT_ASG) && root->AsOp()->gtOp1->OperIs(GT_LCL_VAR) && + root->AsOp()->gtOp1->AsLclVarCommon()->GetLclNum() == lclNum) + { + return root->AsOp()->gtOp2; + } + } + return nullptr; + }; + + GenTree* valueNode = findValue(impStmtList); + BasicBlock* bb = fgFirstBB; + while (valueNode == nullptr && bb != nullptr) + { + valueNode = findValue(bb->bbStmtList); + if (valueNode == nullptr && bb->NumSucc(this) == 1) + { + bb = bb->GetSucc(0, this); + } + else + { + bb = nullptr; + } + } + + if (valueNode != nullptr && valueNode->OperIs(GT_LCL_VAR)) + { + return impGetNodeFromLocal(valueNode); + } + + return valueNode; +} + //------------------------------------------------------------------------ // impImportStaticReadOnlyField: Tries to import 'static readonly' field // as a constant if the host type is statically initialized. diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index e570c99e7537e3..fc3d53020d73e0 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -104,13 +104,17 @@ var_types Compiler::impImportCall(OPCODE opcode, bool checkForSmallType = false; bool bIntrinsicImported = false; - CORINFO_SIG_INFO calliSig; + CORINFO_SIG_INFO originalSig = {}; NewCallArg extraArg; - /*------------------------------------------------------------------------- - * First create the call node - */ + // run transformations when instrumenting to not pollute PGO data + bool optimizedOrInstrumented = opts.OptimizationEnabled() || opts.IsInstrumented(); + CORINFO_METHOD_HANDLE replacementMethod = nullptr; + GenTree* newThis = nullptr; + var_types oldThis = TYP_UNDEF; + var_types targetThis = TYP_UNDEF; + // handle special import cases if (opcode == CEE_CALLI) { if (IsTargetAbi(CORINFO_NATIVEAOT_ABI)) @@ -125,25 +129,107 @@ var_types Compiler::impImportCall(OPCODE opcode, } /* Get the call site sig */ - eeGetSig(pResolvedToken->token, pResolvedToken->tokenScope, pResolvedToken->tokenContext, &calliSig); + eeGetSig(pResolvedToken->token, pResolvedToken->tokenScope, pResolvedToken->tokenContext, &originalSig); + + if (!optimizedOrInstrumented) + { + // ignore + } + else if (originalSig.getCallConv() == CORINFO_CALLCONV_DEFAULT) + { + JITDUMP("\n\nimpImportCall trying to import calli as call\n"); + GenTree* fptr = impStackTop().val; + if (fptr->OperIs(GT_LCL_VAR)) + { + JITDUMP("impImportCall trying to import calli as call - trying to substitute LCL_VAR\n"); + GenTree* lclValue = impGetNodeFromLocal(fptr); + if (lclValue != nullptr) + { + fptr = lclValue; + } + } + if (fptr->OperIs(GT_FTN_ADDR)) + { + replacementMethod = fptr->AsFptrVal()->gtFptrMethod; + } + else + { + JITDUMP("impImportCall failed to import calli as call - address node not found\n"); + } + } + else + { + JITDUMP("\n\nimpImportCall failed to import calli as call - call conv %u is not managed\n", + originalSig.getCallConv()); + } + } + + if (replacementMethod != nullptr) + { + JITDUMP("impImportCall trying to transform call - found target method %s\n", + eeGetMethodName(replacementMethod)); + CORINFO_SIG_INFO methodSig; + CORINFO_CLASS_HANDLE targetClass = info.compCompHnd->getMethodClass(replacementMethod); + info.compCompHnd->getMethodSig(replacementMethod, &methodSig, targetClass); + + if (methodSig.hasImplicitThis() && targetThis == TYP_UNDEF) + { + targetThis = eeIsValueClass(targetClass) ? TYP_BYREF : TYP_REF; + } + + unsigned replacementFlags = info.compCompHnd->getMethodAttribs(replacementMethod); - callRetTyp = JITtype2varType(calliSig.retType); + if ((replacementFlags & CORINFO_FLG_PINVOKE) != 0) + { + JITDUMP("impImportCall aborting transformation - found PInvoke\n"); + } + else if (impCanSubstituteSig(&originalSig, &methodSig, oldThis, targetThis)) + { + impPopStack(); + if (newThis != nullptr) + { + assert(oldThis == TYP_REF); + assert(targetThis == TYP_REF); + CORINFO_CLASS_HANDLE thisCls = NO_CLASS_HANDLE; + info.compCompHnd->getArgType(&methodSig, methodSig.args, &thisCls); + impPushOnStack(newThis, typeInfo(TI_REF, thisCls)); + } + JITDUMP("impImportCall transforming call\n"); + pResolvedToken->hMethod = replacementMethod; + pResolvedToken->hClass = targetClass; + + callInfo->sig = methodSig; + callInfo->hMethod = replacementMethod; + callInfo->methodFlags = replacementFlags; + callInfo->classFlags = info.compCompHnd->getClassAttribs(targetClass); - call = impImportIndirectCall(&calliSig, di); + return impImportCall(CEE_CALL, pResolvedToken, nullptr, nullptr, prefixFlags, callInfo, rawILOffset); + } + } + + /*------------------------------------------------------------------------- + * First create the call node + */ + + if (opcode == CEE_CALLI) + { + callRetTyp = JITtype2varType(originalSig.retType); + + call = impImportIndirectCall(&originalSig, di); // We don't know the target method, so we have to infer the flags, or // assume the worst-case. - mflags = (calliSig.callConv & CORINFO_CALLCONV_HASTHIS) ? 0 : CORINFO_FLG_STATIC; + mflags = (originalSig.callConv & CORINFO_CALLCONV_HASTHIS) ? 0 : CORINFO_FLG_STATIC; #ifdef DEBUG if (verbose) { - unsigned structSize = (callRetTyp == TYP_STRUCT) ? eeTryGetClassSize(calliSig.retTypeSigClass) : 0; + unsigned structSize = (callRetTyp == TYP_STRUCT) ? eeTryGetClassSize(originalSig.retTypeSigClass) : 0; printf("\nIn Compiler::impImportCall: opcode is %s, kind=%d, callRetType is %s, structSize is %u\n", opcodeNames[opcode], callInfo->kind, varTypeName(callRetTyp), structSize); } #endif - sig = &calliSig; + sig = &originalSig; } else // (opcode != CEE_CALLI) { @@ -1622,6 +1708,138 @@ GenTree* Compiler::impFixupCallStructReturn(GenTreeCall* call, CORINFO_CLASS_HAN #endif // FEATURE_MULTIREG_RET } +//----------------------------------------------------------------------------------- +// impCanSubstituteSig: Checks whether it's safe to replace a call with another one. +// This DOES NOT check if the calls are actually compatible, it only checks if their trees are. +// Use ONLY when code will call the method with target sig anyway. +// +// Arguments: +// sourceSig - original call signature +// targetSig - new call signature +// transformation - transformations performed on the original signature +// +// Return Value: +// Whether it's safe to change the IR to call the target method +// +bool Compiler::impCanSubstituteSig(CORINFO_SIG_INFO* sourceSig, + CORINFO_SIG_INFO* targetSig, + var_types sourceThis, + var_types targetThis) +{ + assert(sourceSig->hasImplicitThis() || sourceThis == TYP_UNDEF); + assert(targetSig->hasImplicitThis() || targetThis == TYP_UNDEF || targetThis == TYP_VOID); + assert(!targetSig->hasImplicitThis() || targetThis != TYP_VOID); + assert(sourceThis != TYP_VOID); + + if (sourceSig->getCallConv() != targetSig->getCallConv()) + { + JITDUMP("impCanSubstituteSig returning false - call conv %u != %u\n", sourceSig->callConv, targetSig->callConv); + return false; + } + + if ((sourceSig->hasImplicitThis() && sourceThis == TYP_UNDEF) || + (targetSig->hasImplicitThis() && targetThis == TYP_UNDEF)) + { + JITDUMP("impCanSubstituteSig returning false - unknown this type\n"); + return false; + } + + unsigned sourceArgCount = sourceSig->totalILArgs(); + if (targetThis == TYP_VOID) + { + assert(sourceSig->hasImplicitThis() && sourceThis == TYP_REF); + sourceArgCount--; + } + + if (sourceArgCount != targetSig->totalILArgs()) + { + JITDUMP("impCanSubstituteSig returning false - args count %u != %u\n", sourceArgCount, + targetSig->totalILArgs()); + return false; + } + + if (sourceSig->retType != targetSig->retType) + { + JITDUMP("impCanSubstituteSig returning false - return type %u != %u\n", (unsigned)sourceSig->retType, + (unsigned)targetSig->retType); + return false; + } + + if (sourceSig->retType == CORINFO_TYPE_VALUECLASS || sourceSig->retType == CORINFO_TYPE_REFANY) + { + ClassLayout* layoutRetA = typGetObjLayout(sourceSig->retTypeClass); + ClassLayout* layoutRetB = typGetObjLayout(targetSig->retTypeClass); + + if (!ClassLayout::AreCompatible(layoutRetA, layoutRetB)) + { + JITDUMP("impCanSubstituteSig returning false - return type %u is incompatible with %u\n", + (unsigned)sourceSig->retType, (unsigned)targetSig->retType); + return false; + } + } + + CORINFO_ARG_LIST_HANDLE sourceArg = sourceSig->args; + CORINFO_ARG_LIST_HANDLE targetArg = targetSig->args; + + unsigned numArgs = targetSig->totalILArgs(); + + if ((sourceSig->hasImplicitThis() && targetThis != TYP_VOID) || targetSig->hasImplicitThis()) + { + if (sourceThis == TYP_UNDEF) + { + sourceThis = eeGetArgType(sourceArg, sourceSig); + sourceArg = info.compCompHnd->getArgNext(sourceArg); + numArgs--; + } + else + { + targetThis = eeGetArgType(targetArg, targetSig); + targetArg = info.compCompHnd->getArgNext(targetArg); + } + assert(sourceThis == TYP_REF || sourceThis == TYP_BYREF || sourceThis == TYP_I_IMPL); + assert(targetThis == TYP_REF || targetThis == TYP_BYREF || targetThis == TYP_I_IMPL); + if (sourceThis != targetThis) + { + JITDUMP("impCanSubstituteSig returning false - this type %s != %s\n", varTypeName(sourceThis), + varTypeName(targetThis)); + return false; + } + } + + for (unsigned i = 0; i < numArgs; + i++, sourceArg = info.compCompHnd->getArgNext(sourceArg), targetArg = info.compCompHnd->getArgNext(targetArg)) + { + var_types sourceType = eeGetArgType(sourceArg, sourceSig); + var_types targetType = eeGetArgType(targetArg, targetSig); + if (sourceType != targetType) + { + JITDUMP("impCanSubstituteSig returning false - parameter %u type %s != %s\n", i, varTypeName(sourceType), + varTypeName(targetType)); + return false; + } + + if (varTypeIsStruct(sourceType) && varTypeIsStruct(targetType)) + { + CORINFO_CLASS_HANDLE sourceClassHnd = NO_CLASS_HANDLE; + CORINFO_CLASS_HANDLE targetClassHnd = NO_CLASS_HANDLE; + info.compCompHnd->getArgType(sourceSig, sourceArg, &sourceClassHnd); + info.compCompHnd->getArgType(targetSig, targetArg, &targetClassHnd); + + ClassLayout* sourceLayout = typGetObjLayout(sourceClassHnd); + ClassLayout* targetLayout = typGetObjLayout(targetClassHnd); + + if (!ClassLayout::AreCompatible(sourceLayout, targetLayout)) + { + JITDUMP("impCanSubstituteSig returning false - parameter %u type %s is inconmpatible with %s\n", i, + varTypeName(sourceType), varTypeName(targetType)); + return false; + } + } + } + + return true; +} + GenTreeCall* Compiler::impImportIndirectCall(CORINFO_SIG_INFO* sig, const DebugInfo& di) { var_types callRetTyp = JITtype2varType(sig->retType); diff --git a/src/tests/JIT/opt/Devirtualization/CMakeLists.txt b/src/tests/JIT/opt/Devirtualization/CMakeLists.txt new file mode 100644 index 00000000000000..02f46f6c126c5a --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/CMakeLists.txt @@ -0,0 +1,5 @@ +project (IndirectNative) +set(SOURCES IndirectNative.cpp) +add_library (IndirectNative SHARED ${SOURCES}) +# add the install targets +install (TARGETS IndirectNative DESTINATION bin) diff --git a/src/tests/JIT/opt/Devirtualization/Indirect.cs b/src/tests/JIT/opt/Devirtualization/Indirect.cs new file mode 100644 index 00000000000000..6637dc68a3d332 --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/Indirect.cs @@ -0,0 +1,322 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +public static unsafe class Program +{ + public static int Main() + { + _ = Test.ExecuteCctor(); + Test.Run(); + return 100; + } +} + +public static unsafe class Test +{ + // valid ptrs + private static readonly delegate* Ptr = &A; + + // invalid ptrs + private static readonly delegate* PtrNull = (delegate*)0; + private static readonly delegate* PtrPlus1 = (delegate*)(((nuint)(delegate*)(&A)) + 1); + private static readonly delegate* PtrMinus1 = (delegate*)(((nuint)(delegate*)(&A)) - 1); + private static readonly delegate* PtrPlus16 = (delegate*)(((nuint)(delegate*)(&A)) + 16); + private static readonly delegate* PtrMinus16 = (delegate*)(((nuint)(delegate*)(&A)) - 16); + private static readonly delegate* PtrPlus32 = (delegate*)(((nuint)(delegate*)(&A)) + 32); + private static readonly delegate* PtrMinus32 = (delegate*)(((nuint)(delegate*)(&A)) - 32); + private static readonly delegate* PtrSmall = (delegate*)4096; + private static readonly delegate* PtrDeadBeef = (delegate*)0xDEADBEEFU; + + // valid but failing checks + private static readonly delegate* PtrWrongSig = (delegate*)(delegate*)&C; + + private static int A() => 1; + private static int B() => 2; + private static int A(int a, int b) => a + b + 1; + private static int B(int a, int b) => a + b + 2; + private static void C() { } + private static void C(int a, int b) { } + + private static int D() => 3; + [UnmanagedCallersOnly] + private static int UnmanagedDefault() => D(); + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int UnmanagedCdecl() => D(); + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + private static int UnmanagedStdcall() => D(); + + private static int D(int a, int b) => a + b + 3; + [UnmanagedCallersOnly] + private static int UnmanagedDefault(int a, int b) => D(a, b); + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int UnmanagedCdecl(int a, int b) => D(a, b); + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] + private static int UnmanagedStdcall(int a, int b) => D(a, b); + + [DllImport("IndirectNative", EntryPoint = "E")] + private static extern int E(); + [DllImport("IndirectNative", EntryPoint = "EParam")] + private static extern int E(int a, int b); + [DllImport("IndirectNative", EntryPoint = "EPtrs")] + private static extern int E(ref int a, ref int b); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static delegate* ExecuteCctor() => Ptr; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Run() + { + AssertThrowsNullReferenceException(() => ((delegate*)0)()); + AssertThrowsNullReferenceException(() => PtrNull()); + + AreSame(A(), Invoke(() => ((delegate*)&A)())); + AreSame(B(), Invoke(() => ((delegate*)&B)())); + AreSame(A(), Invoke(() => { + ((delegate*)&C)(); + return A(); + })); + + AreSame(D(), Invoke(() => ((delegate* unmanaged)&UnmanagedDefault)())); + AreSame(D(), Invoke(() => ((delegate* unmanaged[Cdecl])&UnmanagedCdecl)())); + AreSame(D(), Invoke(() => ((delegate* unmanaged[Stdcall])&UnmanagedStdcall)())); + + AreSame(E(), Invoke(() => ((delegate*)&E)())); + + AreSame(A(8, 9), Invoke(() => ((delegate*)&A)(8, 9))); + AreSame(B(8, 9), Invoke(() => ((delegate*)&B)(8, 9))); + AreSame(A(8, 9), Invoke(() => { + ((delegate*)&C)(8, 9); + return A(8, 9); + })); + + AreSame(D(8, 9), Invoke(() => ((delegate* unmanaged)&UnmanagedDefault)(8, 9))); + AreSame(D(8, 9), Invoke(() => ((delegate* unmanaged[Cdecl])&UnmanagedCdecl)(8, 9))); + AreSame(D(8, 9), Invoke(() => ((delegate* unmanaged[Stdcall])&UnmanagedStdcall)(8, 9))); + + AreSame(E(8, 9), Invoke(() => ((delegate*)&E)(8, 9))); + + AreSame(E(8, 9), Invoke(() => { + int a = 8; + int b = 9; + return ((delegate*)&E)(ref a, ref b); + })); + + AreSame(A(), Invoke(() => Ptr())); + + static int CallPtr(delegate* ptr) => ptr(); + AreSame(A(), Invoke(() => CallPtr(&A))); + AreSame(B(), Invoke(() => CallPtr(&B))); + + static delegate* ReturnA() => &A; + AreSame(A(), Invoke(() => ReturnA()())); + static delegate* ReturnAStatic() => Ptr; + AreSame(A(), Invoke(() => ReturnAStatic()())); + + static int CallPtrParam(delegate* ptr, int a, int b) => ptr(a, b); + AreSame(A(8, 9), Invoke(() => CallPtrParam(&A, 8, 9))); + AreSame(B(8, 9), Invoke(() => CallPtrParam(&B, 8, 9))); + + static delegate* ReturnAParam() => &A; + AreSame(A(8, 9), Invoke(() => ReturnAParam()(8, 9))); + + AreSame(A(), Invoke(() => + { + var ptr = (delegate*)&A; + _ = B(); + return ptr(); + })); + AreSame(A(), Invoke(() => + { + var ptr = (delegate*)&A; + _ = NoInline(B()); + return ptr(); + })); + + AreSame(A(8, 9), Invoke(() => + { + var ptr = (delegate*)&A; + _ = B(8, 9); + return ptr(8, 9); + })); + AreSame(A(8, 9), Invoke(() => + { + var ptr = (delegate*)&A; + _ = NoInline(B(8, 9)); + return ptr(8, 9); + })); + + static int Branch(bool a) + { + delegate* ptr; + if (a) + ptr = &A; + else + ptr = &B; + return ptr(); + } + + AreSame(A(), Invoke(() => Branch(true))); + AreSame(B(), Invoke(() => Branch(false))); + AreSame(A(), Invoke(a => Branch(a), true)); + AreSame(B(), Invoke(a => Branch(a), false)); + + static int BranchParam(bool a) + { + delegate* ptr; + if (a) + ptr = &A; + else + ptr = &B; + return ptr(8, 9); + } + + AreSame(A(8, 9), Invoke(() => BranchParam(true))); + AreSame(B(8, 9), Invoke(() => BranchParam(false))); + AreSame(A(8, 9), Invoke(a => BranchParam(a), true)); + AreSame(B(8, 9), Invoke(a => BranchParam(a), false)); + + AreSame(A(), Invoke(a => a ? PtrNull() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrPlus1() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrMinus1() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrPlus16() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrMinus16() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrPlus32() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrMinus32() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrSmall() : Ptr(), false)); + AreSame(A(), Invoke(a => a ? PtrDeadBeef() : Ptr(), false)); + + AreSame(A(), Invoke(a => a ? PtrWrongSig() : Ptr(), false)); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void Assign(delegate** ptrptr, delegate* ptr) => *ptrptr = ptr; + + AreSame(A(), Invoke(() => + { + delegate* ptr = &A; + Assign(&ptr, &B); + return ptr(); + })); + AreSame(A(), Invoke(() => + { + delegate* ptr; + Assign(&ptr, &B); + ptr = &A; + return ptr(); + })); + + static int ConditionalAddressAssign(bool a) + { + delegate* ptr = &A; + if (a) + Assign(&ptr, &B); + return ptr(); + } + + AreSame(A(), Invoke(() => ConditionalAddressAssign(false))); + AreSame(B(), Invoke(() => ConditionalAddressAssign(true))); + AreSame(A(), Invoke(a => ConditionalAddressAssign(a), false)); + AreSame(B(), Invoke(a => ConditionalAddressAssign(a), true)); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void AssignParam(delegate** ptrptr, delegate* ptr) => *ptrptr = ptr; + + AreSame(A(8, 9), Invoke(() => + { + delegate* ptr = &A; + AssignParam(&ptr, &B); + return ptr(8, 9); + })); + AreSame(A(8, 9), Invoke(() => + { + delegate* ptr; + AssignParam(&ptr, &B); + ptr = &A; + return ptr(8, 9); + })); + + static int ConditionalAddressAssignParam(bool a, int b, int c) + { + delegate* ptr = &A; + if (a) + AssignParam(&ptr, &B); + return ptr(b, c); + } + + AreSame(A(), Invoke(() => ConditionalAddressAssignParam(false, 8, 9))); + AreSame(B(), Invoke(() => ConditionalAddressAssignParam(true, 8, 9))); + AreSame(A(), Invoke(a => ConditionalAddressAssignParam(a, 8, 9), false)); + AreSame(B(), Invoke(a => ConditionalAddressAssignParam(a, 8, 9), true)); + + AreSame(2, IndirectIL.StaticClass()); + AreSame(1, IndirectIL.InstanceClass()); + AreSame(1, IndirectIL.InstanceExplicitClass()); + AreSame(4, IndirectIL.StaticClassParam()); + AreSame(3, IndirectIL.InstanceClassParam()); + AreSame(3, IndirectIL.InstanceExplicitClassParam()); + + AreSame(2, IndirectIL.StaticStruct()); + AreSame(1, IndirectIL.InstanceStruct()); + AreSame(1, IndirectIL.InstanceExplicitStruct()); + AreSame(4, IndirectIL.StaticStructParam()); + AreSame(3, IndirectIL.InstanceStructParam()); + AreSame(3, IndirectIL.InstanceExplicitStructParam()); + + AreSame(2, Invoke(() => IndirectIL.BranchAssign(true))); + AreSame(2, Invoke((a) => IndirectIL.BranchAssign(a), true)); + AssertThrowsNullReferenceException(() => IndirectIL.BranchAssign(false)); + AssertThrowsNullReferenceException((a) => IndirectIL.BranchAssign(a), false); + + AssertThrowsNullReferenceException(() => IndirectIL.NoAssign()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T Invoke(Func action) => action(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TRet Invoke(Func action, T value) => action(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T NoInline(T value) => value; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AreSame(T expected, T actual, [CallerLineNumber] int line = 0) + { + if (!EqualityComparer.Default.Equals(expected, actual)) + throw new InvalidOperationException($"Invalid value, expected {expected}, got {actual} at line {line}"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AssertThrowsNullReferenceException(Func a, [CallerLineNumber] int line = 0) + { + try + { + _ = a(); + } + catch (NullReferenceException) + { + return; + } + + throw new InvalidOperationException($"Expected NullReferenceException at line {line}"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AssertThrowsNullReferenceException(Func a, TArg arg, [CallerLineNumber] int line = 0) + { + try + { + _ = a(arg); + } + catch (NullReferenceException) + { + return; + } + + throw new InvalidOperationException($"Expected NullReferenceException at line {line}"); + } +} diff --git a/src/tests/JIT/opt/Devirtualization/Indirect.csproj b/src/tests/JIT/opt/Devirtualization/Indirect.csproj new file mode 100644 index 00000000000000..45d9d2a00b87d0 --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/Indirect.csproj @@ -0,0 +1,17 @@ + + + Exe + 1 + True + true + + + + + + + + + + + diff --git a/src/tests/JIT/opt/Devirtualization/IndirectIL.il b/src/tests/JIT/opt/Devirtualization/IndirectIL.il new file mode 100644 index 00000000000000..e7fa3f010eb42a --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/IndirectIL.il @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) } + +.assembly IndirectIL { } + +.class public abstract auto ansi sealed beforefieldinit ILTests + extends [System.Runtime]System.Object +{ + .method public hidebysig static int32 StaticClass() cil managed noinlining aggressiveoptimization + { + // Code size 12 (0xc) + .maxstack 8 + IL_0000: ldftn int32 Cl::B() + IL_0006: calli int32() + IL_000b: ret + } // end of method ILTests::StaticClass + + .method public hidebysig static int32 InstanceClass() cil managed noinlining aggressiveoptimization + { + // Code size 17 (0x11) + .maxstack 2 + .locals init (class Cl V_0) + IL_0000: newobj instance void Cl::.ctor() + IL_0005: ldftn instance int32 Cl::A() + IL_000b: calli instance int32() + IL_0010: ret + } // end of method ILTests::InstanceClass + + .method public hidebysig static int32 InstanceExplicitClass() cil managed noinlining aggressiveoptimization + { + // Code size 17 (0x11) + .maxstack 2 + .locals init (class Cl V_0) + IL_0000: newobj instance void Cl::.ctor() + IL_0005: ldftn instance int32 Cl::A() + IL_000b: calli explicit instance int32(class Cl) + IL_0010: ret + } // end of method ILTests::InstanceExplicitClass + + .method public hidebysig static int32 StaticClassParam() cil managed noinlining aggressiveoptimization + { + // Code size 15 (0xf) + .maxstack 2 + .locals init (method int32 *(int32) V_0) + IL_0000: ldftn int32 Cl::D(int32) + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: ldloc.0 + IL_0009: calli int32(int32) + IL_000e: ret + } // end of method ILTests::StaticClassParam + + .method public hidebysig static int32 InstanceClassParam() cil managed noinlining aggressiveoptimization + { + // Code size 22 (0x16) + .maxstack 3 + .locals init (class Cl V_0, + method int32 *(int32) V_1) + IL_0000: newobj instance void Cl::.ctor() + IL_0005: stloc.0 + IL_0006: ldftn instance int32 Cl::C(int32) + IL_000c: stloc.1 + IL_000d: ldloc.0 + IL_000e: ldc.i4.0 + IL_000f: ldloc.1 + IL_0010: calli instance int32(int32) + IL_0015: ret + } // end of method ILTests::InstanceClassParam + + .method public hidebysig static int32 InstanceExplicitClassParam() cil managed noinlining aggressiveoptimization + { + // Code size 22 (0x16) + .maxstack 3 + .locals init (class Cl V_0, + method int32 *(int32) V_1) + IL_0000: newobj instance void Cl::.ctor() + IL_0005: stloc.0 + IL_0006: ldftn instance int32 Cl::C(int32) + IL_000c: stloc.1 + IL_000d: ldloc.0 + IL_000e: ldc.i4.0 + IL_000f: ldloc.1 + IL_0010: calli explicit instance int32(class Cl,int32) + IL_0015: ret + } // end of method ILTests::InstanceExplicitClassParam + + .method public hidebysig static int32 StaticStruct() cil managed noinlining aggressiveoptimization + { + // Code size 12 (0xc) + .maxstack 8 + IL_0000: ldftn int32 St::B() + IL_0006: calli int32() + IL_000b: ret + } // end of method ILTests::StaticStruct + + .method public hidebysig static int32 InstanceStruct() cil managed noinlining aggressiveoptimization + { + // Code size 22 (0x16) + .maxstack 2 + .locals init (valuetype St V_0) + IL_0000: ldloca.s V_0 + IL_0002: initobj St + IL_0008: ldloca.s V_0 + IL_000a: ldftn instance int32 St::A() + IL_0010: calli instance int32() + IL_0015: ret + } // end of method ILTests::InstanceStruct + + .method public hidebysig static int32 InstanceExplicitStruct() cil managed noinlining aggressiveoptimization + { + // Code size 22 (0x16) + .maxstack 2 + .locals init (valuetype St V_0) + IL_0000: ldloca.s V_0 + IL_0002: initobj St + IL_0008: ldloca.s V_0 + IL_000a: ldftn instance int32 St::A() + IL_0010: calli explicit instance int32(valuetype St&) + IL_0015: ret + } // end of method ILTests::InstanceExplicitStruct + + .method public hidebysig static int32 StaticStructParam() cil managed noinlining aggressiveoptimization + { + // Code size 15 (0xf) + .maxstack 2 + .locals init (method int32 *(int32) V_0) + IL_0000: ldftn int32 St::D(int32) + IL_0006: stloc.0 + IL_0007: ldc.i4.0 + IL_0008: ldloc.0 + IL_0009: calli int32(int32) + IL_000e: ret + } // end of method ILTests::StaticStructParam + + .method public hidebysig static int32 InstanceStructParam() cil managed noinlining aggressiveoptimization + { + // Code size 25 (0x19) + .maxstack 3 + .locals init (valuetype St V_0, + method int32 *(int32) V_1) + IL_0000: ldloca.s V_0 + IL_0002: initobj St + IL_0008: ldftn instance int32 St::C(int32) + IL_000e: stloc.1 + IL_000f: ldloca.s V_0 + IL_0011: ldc.i4.0 + IL_0012: ldloc.1 + IL_0013: calli instance int32(int32) + IL_0018: ret + } // end of method ILTests::InstanceStructParam + + .method public hidebysig static int32 InstanceExplicitStructParam() cil managed noinlining aggressiveoptimization + { + // Code size 25 (0x19) + .maxstack 3 + .locals init (valuetype St V_0, + method int32 *(int32) V_1) + IL_0000: ldloca.s V_0 + IL_0002: initobj St + IL_0008: ldftn instance int32 St::C(int32) + IL_000e: stloc.1 + IL_000f: ldloca.s V_0 + IL_0011: ldc.i4.0 + IL_0012: ldloc.1 + IL_0013: calli explicit instance int32(valuetype St&,int32) + IL_0018: ret + } // end of method ILTests::InstanceExplicitStructParam + + .method public hidebysig static + int32 BranchAssign ( + bool b + ) cil managed + { + // Method begins at RVA 0x2050 + // Code size 24 (0x18) + .maxstack 1 + .locals init ( + [0] method int32 *() + ) + + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_0011 + + IL_0003: ldftn int32 Cl::B() + IL_0009: stloc.0 + IL_000a: ldloc.0 + IL_000b: calli int32() + IL_0010: ret + + IL_0011: ldloc.0 + IL_0012: calli int32() + IL_0017: ret + } // end of method ILTests::BranchAssign + + .method public hidebysig static + int32 NoAssign () cil managed + { + // Method begins at RVA 0x2050 + // Code size 7 (0x7) + .maxstack 1 + .locals init ( + [0] method int32 *() + ) + + IL_0000: ldloc.0 + IL_0001: calli int32() + IL_0006: ret + } // end of method ILTests::NoAssign +} // end of class ILTests + +.class public auto ansi sealed beforefieldinit Cl + extends [System.Runtime]System.Object +{ + .method public hidebysig instance int32 + A() cil managed aggressiveinlining aggressiveoptimization + { + // Code size 2 (0x2) + .maxstack 8 + IL_0000: ldc.i4.1 + IL_0001: ret + } // end of method Cl::A + + .method public hidebysig static int32 B() cil managed aggressiveinlining aggressiveoptimization + { + // Code size 2 (0x2) + .maxstack 8 + IL_0000: ldc.i4.2 + IL_0001: ret + } // end of method Cl::B + + .method public hidebysig instance int32 + C(int32 a) cil managed aggressiveinlining aggressiveoptimization + { + // Code size 4 (0x4) + .maxstack 8 + IL_0000: ldc.i4.3 + IL_0001: ldarg.1 + IL_0002: add + IL_0003: ret + } // end of method Cl::C + + .method public hidebysig static int32 D(int32 a) cil managed aggressiveinlining aggressiveoptimization + { + // Code size 4 (0x4) + .maxstack 8 + IL_0000: ldc.i4.4 + IL_0001: ldarg.0 + IL_0002: add + IL_0003: ret + } // end of method Cl::D + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + // Code size 7 (0x7) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + } // end of method Cl::.ctor + +} // end of class Cl + +.class public sequential ansi sealed beforefieldinit St + extends [System.Runtime]System.ValueType +{ + .pack 0 + .size 1 + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) + .method public hidebysig instance int32 + A() cil managed aggressiveinlining aggressiveoptimization + { + // Code size 2 (0x2) + .maxstack 8 + IL_0000: ldc.i4.1 + IL_0001: ret + } // end of method St::A + + .method public hidebysig static int32 B() cil managed aggressiveinlining aggressiveoptimization + { + // Code size 2 (0x2) + .maxstack 8 + IL_0000: ldc.i4.2 + IL_0001: ret + } // end of method St::B + + .method public hidebysig instance int32 + C(int32 a) cil managed aggressiveinlining aggressiveoptimization + { + // Code size 4 (0x4) + .maxstack 8 + IL_0000: ldc.i4.3 + IL_0001: ldarg.1 + IL_0002: add + IL_0003: ret + } // end of method St::C + + .method public hidebysig static int32 D(int32 a) cil managed aggressiveinlining aggressiveoptimization + { + // Code size 4 (0x4) + .maxstack 8 + IL_0000: ldc.i4.4 + IL_0001: ldarg.0 + IL_0002: add + IL_0003: ret + } // end of method St::D + +} // end of class St diff --git a/src/tests/JIT/opt/Devirtualization/IndirectIL.ilproj b/src/tests/JIT/opt/Devirtualization/IndirectIL.ilproj new file mode 100644 index 00000000000000..05fbf28a4e307c --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/IndirectIL.ilproj @@ -0,0 +1,11 @@ + + + Library + true + BuildOnly + false + + + + + diff --git a/src/tests/JIT/opt/Devirtualization/IndirectNative.cpp b/src/tests/JIT/opt/Devirtualization/IndirectNative.cpp new file mode 100644 index 00000000000000..bfa7ed3a5439fc --- /dev/null +++ b/src/tests/JIT/opt/Devirtualization/IndirectNative.cpp @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include + +#ifdef TARGET_WINDOWS +#define DLL_EXPORT extern "C" __declspec(dllexport) +#else +#define DLL_EXPORT extern "C" __attribute((visibility("default"))) +#endif + +#ifndef TARGET_WINDOWS +#define __stdcall +#endif + +DLL_EXPORT int32_t __stdcall E() +{ + return 4; +} + +DLL_EXPORT int32_t __stdcall EParams(int32_t a, int32_t b) +{ + return a + b + 4; +} + +DLL_EXPORT int32_t __stdcall EPtrs(int32_t* a, int32_t* b) +{ + return *a + *b + 4; +}