From a06b447db67a6a28ef93068cf9e12022425e29f8 Mon Sep 17 00:00:00 2001 From: Sergey Andreenko Date: Wed, 12 Feb 2020 16:23:56 -0800 Subject: [PATCH] Implement the JIT part of the dynamic dictionary expansion feature. (#31957) * Implement the JIT part of the dynamic dictionary expansion feature. * review response. * response review --- src/coreclr/src/jit/compiler.cpp | 15 ++ src/coreclr/src/jit/compiler.h | 36 ++- src/coreclr/src/jit/gentree.cpp | 4 +- src/coreclr/src/jit/gentree.h | 16 ++ src/coreclr/src/jit/importer.cpp | 70 ++++-- .../src/jit/indirectcalltransformer.cpp | 224 +++++++++++++++++- 6 files changed, 326 insertions(+), 39 deletions(-) diff --git a/src/coreclr/src/jit/compiler.cpp b/src/coreclr/src/jit/compiler.cpp index 6ea252b87a622..c066d2b1be9f1 100644 --- a/src/coreclr/src/jit/compiler.cpp +++ b/src/coreclr/src/jit/compiler.cpp @@ -9102,6 +9102,21 @@ void cTreeFlags(Compiler* comp, GenTree* tree) { chars += printf("[CALL_M_PINVOKE]"); } + + if (call->IsFatPointerCandidate()) + { + chars += printf("[CALL_FAT_POINTER_CANDIDATE]"); + } + + if (call->IsGuarded()) + { + chars += printf("[CALL_GUARDED]"); + } + + if (call->IsExpRuntimeLookup()) + { + chars += printf("[CALL_EXP_RUNTIME_LOOKUP]"); + } } break; default: diff --git a/src/coreclr/src/jit/compiler.h b/src/coreclr/src/jit/compiler.h index 112ceea2070e0..d8aff229e8156 100644 --- a/src/coreclr/src/jit/compiler.h +++ b/src/coreclr/src/jit/compiler.h @@ -2447,7 +2447,7 @@ class Compiler // For binary opers. GenTree* gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, GenTree* op2); - GenTree* gtNewQmarkNode(var_types type, GenTree* cond, GenTree* colon); + GenTreeQmark* gtNewQmarkNode(var_types type, GenTree* cond, GenTree* colon); GenTree* gtNewLargeOperNode(genTreeOps oper, var_types type = TYP_I_IMPL, @@ -6326,14 +6326,15 @@ class Compiler } }; -#define OMF_HAS_NEWARRAY 0x00000001 // Method contains 'new' of an array -#define OMF_HAS_NEWOBJ 0x00000002 // Method contains 'new' of an object type. -#define OMF_HAS_ARRAYREF 0x00000004 // Method contains array element loads or stores. -#define OMF_HAS_VTABLEREF 0x00000008 // Method contains method table reference. -#define OMF_HAS_NULLCHECK 0x00000010 // Method contains null check. -#define OMF_HAS_FATPOINTER 0x00000020 // Method contains call, that needs fat pointer transformation. -#define OMF_HAS_OBJSTACKALLOC 0x00000040 // Method contains an object allocated on the stack. -#define OMF_HAS_GUARDEDDEVIRT 0x00000080 // Method contains guarded devirtualization candidate +#define OMF_HAS_NEWARRAY 0x00000001 // Method contains 'new' of an array +#define OMF_HAS_NEWOBJ 0x00000002 // Method contains 'new' of an object type. +#define OMF_HAS_ARRAYREF 0x00000004 // Method contains array element loads or stores. +#define OMF_HAS_VTABLEREF 0x00000008 // Method contains method table reference. +#define OMF_HAS_NULLCHECK 0x00000010 // Method contains null check. +#define OMF_HAS_FATPOINTER 0x00000020 // Method contains call, that needs fat pointer transformation. +#define OMF_HAS_OBJSTACKALLOC 0x00000040 // Method contains an object allocated on the stack. +#define OMF_HAS_GUARDEDDEVIRT 0x00000080 // Method contains guarded devirtualization candidate +#define OMF_HAS_EXPRUNTIMELOOKUP 0x00000100 // Method contains a runtime lookup to an expandable dictionary. bool doesMethodHaveFatPointer() { @@ -6373,6 +6374,23 @@ class Compiler unsigned methodAttr, unsigned classAttr); + bool doesMethodHaveExpRuntimeLookup() + { + return (optMethodFlags & OMF_HAS_EXPRUNTIMELOOKUP) != 0; + } + + void setMethodHasExpRuntimeLookup() + { + optMethodFlags |= OMF_HAS_EXPRUNTIMELOOKUP; + } + + void clearMethodHasExpRuntimeLookup() + { + optMethodFlags &= ~OMF_HAS_EXPRUNTIMELOOKUP; + } + + void addExpRuntimeLookupCandidate(GenTreeCall* call); + unsigned optMethodFlags; bool doesMethodHaveNoReturnCalls() diff --git a/src/coreclr/src/jit/gentree.cpp b/src/coreclr/src/jit/gentree.cpp index 1bfb4bd006ee1..a9aac9d6d4c22 100644 --- a/src/coreclr/src/jit/gentree.cpp +++ b/src/coreclr/src/jit/gentree.cpp @@ -5670,11 +5670,11 @@ GenTree* Compiler::gtNewOperNode(genTreeOps oper, var_types type, GenTree* op1, return node; } -GenTree* Compiler::gtNewQmarkNode(var_types type, GenTree* cond, GenTree* colon) +GenTreeQmark* Compiler::gtNewQmarkNode(var_types type, GenTree* cond, GenTree* colon) { compQmarkUsed = true; cond->gtFlags |= GTF_RELOP_QMARK; - GenTree* result = new (this, GT_QMARK) GenTreeQmark(type, cond, colon, this); + GenTreeQmark* result = new (this, GT_QMARK) GenTreeQmark(type, cond, colon, this); #ifdef DEBUG if (compQmarkRationalized) { diff --git a/src/coreclr/src/jit/gentree.h b/src/coreclr/src/jit/gentree.h index 8d5578190eea0..755b5ee4c5d0f 100644 --- a/src/coreclr/src/jit/gentree.h +++ b/src/coreclr/src/jit/gentree.h @@ -3919,6 +3919,7 @@ struct GenTreeCall final : public GenTree #define GTF_CALL_M_GUARDED 0x00200000 // GT_CALL -- this call was transformed by guarded devirtualization #define GTF_CALL_M_ALLOC_SIDE_EFFECTS 0x00400000 // GT_CALL -- this is a call to an allocator with side effects #define GTF_CALL_M_SUPPRESS_GC_TRANSITION 0x00800000 // GT_CALL -- suppress the GC transition (i.e. during a pinvoke) but a separate GC safe point is required. +#define GTF_CALL_M_EXP_RUNTIME_LOOKUP 0x01000000 // GT_CALL -- this call needs to be tranformed into CFG for the dynamic dictionary expansion feature. // clang-format on @@ -4168,6 +4169,21 @@ struct GenTreeCall final : public GenTree gtCallMoreFlags |= GTF_CALL_M_GUARDED; } + void SetExpRuntimeLookup() + { + gtFlags |= GTF_CALL_M_EXP_RUNTIME_LOOKUP; + } + + void ClearExpRuntimeLookup() + { + gtFlags &= ~GTF_CALL_M_EXP_RUNTIME_LOOKUP; + } + + bool IsExpRuntimeLookup() const + { + return (gtFlags & GTF_CALL_M_EXP_RUNTIME_LOOKUP) != 0; + } + unsigned gtCallMoreFlags; // in addition to gtFlags unsigned char gtCallType : 3; // value from the gtCallTypes enumeration diff --git a/src/coreclr/src/jit/importer.cpp b/src/coreclr/src/jit/importer.cpp index d2a5e124bad62..0ce15a9ab39d0 100644 --- a/src/coreclr/src/jit/importer.cpp +++ b/src/coreclr/src/jit/importer.cpp @@ -2061,7 +2061,8 @@ GenTree* Compiler::impRuntimeLookupToTree(CORINFO_RESOLVED_TOKEN* pResolvedToken nullptr DEBUGARG("impRuntimeLookup slot")); } - GenTree* indOffTree = nullptr; + GenTree* indOffTree = nullptr; + GenTree* lastIndOfTree = nullptr; // Applied repeated indirections for (WORD i = 0; i < pRuntimeLookup->indirections; i++) @@ -2086,6 +2087,15 @@ GenTree* Compiler::impRuntimeLookupToTree(CORINFO_RESOLVED_TOKEN* pResolvedToken if (pRuntimeLookup->offsets[i] != 0) { +// The last indirection could be subject to a size check (dynamic dictionary expansion feature) +#if 0 // Uncomment that block when you add sizeOffset field to pRuntimeLookup. + if (i == pRuntimeLookup->indirections - 1 && pRuntimeLookup->sizeOffset != 0xFFFF) + { + lastIndOfTree = impCloneExpr(slotPtrTree, &slotPtrTree, NO_CLASS_HANDLE, (unsigned)CHECK_SPILL_ALL, + nullptr DEBUGARG("impRuntimeLookup indirectOffset")); + } +#endif // 0 + slotPtrTree = gtNewOperNode(GT_ADD, TYP_I_IMPL, slotPtrTree, gtNewIconNode(pRuntimeLookup->offsets[i], TYP_I_IMPL)); } @@ -2140,38 +2150,56 @@ GenTree* Compiler::impRuntimeLookupToTree(CORINFO_RESOLVED_TOKEN* pResolvedToken impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("bubbling QMark1")); // Extract the handle - GenTree* handle = gtNewOperNode(GT_IND, TYP_I_IMPL, slotPtrTree); - handle->gtFlags |= GTF_IND_NONFAULTING; - - GenTree* handleCopy = impCloneExpr(handle, &handle, NO_CLASS_HANDLE, (unsigned)CHECK_SPILL_ALL, - nullptr DEBUGARG("impRuntimeLookup typehandle")); + GenTree* handleForNullCheck = gtNewOperNode(GT_IND, TYP_I_IMPL, slotPtrTree); + handleForNullCheck->gtFlags |= GTF_IND_NONFAULTING; // Call to helper GenTree* argNode = gtNewIconEmbHndNode(pRuntimeLookup->signature, nullptr, GTF_ICON_TOKEN_HDL, compileTimeHandle); GenTreeCall::Use* helperArgs = gtNewCallArgs(ctxTree, argNode); - GenTree* helperCall = gtNewHelperCallNode(pRuntimeLookup->helper, TYP_I_IMPL, helperArgs); + GenTreeCall* helperCall = gtNewHelperCallNode(pRuntimeLookup->helper, TYP_I_IMPL, helperArgs); // Check for null and possibly call helper - GenTree* relop = gtNewOperNode(GT_NE, TYP_INT, handle, gtNewIconNode(0, TYP_I_IMPL)); + GenTree* nullCheck = gtNewOperNode(GT_NE, TYP_INT, handleForNullCheck, gtNewIconNode(0, TYP_I_IMPL)); + GenTree* handleForResult = gtCloneExpr(handleForNullCheck); - GenTree* colon = new (this, GT_COLON) GenTreeColon(TYP_I_IMPL, - gtNewNothingNode(), // do nothing if nonnull - helperCall); + GenTree* result = nullptr; - GenTree* qmark = gtNewQmarkNode(TYP_I_IMPL, relop, colon); - - unsigned tmp; - if (handleCopy->IsLocal()) +#if 0 // Uncomment that block when you add sizeOffset field to pRuntimeLookup. + if (pRuntimeLookup->sizeOffset != 0xFFFF) // dynamic dictionary expansion feature { - tmp = handleCopy->AsLclVarCommon()->GetLclNum(); + assert((lastIndOfTree != nullptr) && (pRuntimeLookup->indirections > 0)); + + // sizeValue = dictionary[pRuntimeLookup->sizeOffset] + GenTreeIntCon* sizeOffset = gtNewIconNode(pRuntimeLookup->sizeOffset, TYP_I_IMPL); + GenTree* sizeValueOffset = gtNewOperNode(GT_ADD, TYP_I_IMPL, lastIndOfTree, sizeOffset); + GenTree* sizeValue = gtNewOperNode(GT_IND, TYP_I_IMPL, sizeValueOffset); + + // sizeCheck fails if sizeValue < pRuntimeLookup->offsets[i] + GenTree* offsetValue = gtNewIconNode(pRuntimeLookup->offsets[pRuntimeLookup->indirections - 1], TYP_I_IMPL); + GenTree* sizeCheck = gtNewOperNode(GT_LE, TYP_INT, sizeValue, offsetValue); + + // revert null check condition. + nullCheck->ChangeOperUnchecked(GT_EQ); + + // ((sizeCheck fails || nullCheck fails))) ? (helperCall : handle). + // Add checks and the handle as call arguments, indirect call transformer will handle this. + helperCall->gtCallArgs = gtPrependNewCallArg(handleForResult, helperCall->gtCallArgs); + helperCall->gtCallArgs = gtPrependNewCallArg(sizeCheck, helperCall->gtCallArgs); + helperCall->gtCallArgs = gtPrependNewCallArg(nullCheck, helperCall->gtCallArgs); + result = helperCall; + addExpRuntimeLookupCandidate(helperCall); } else +#endif // 0 { - tmp = lvaGrabTemp(true DEBUGARG("spilling QMark1")); + GenTreeColon* colonNullCheck = new (this, GT_COLON) GenTreeColon(TYP_I_IMPL, handleForResult, helperCall); + result = gtNewQmarkNode(TYP_I_IMPL, nullCheck, colonNullCheck); } - impAssignTempGen(tmp, qmark, (unsigned)CHECK_SPILL_NONE); + unsigned tmp = lvaGrabTemp(true DEBUGARG("spilling Runtime Lookup tree")); + + impAssignTempGen(tmp, result, (unsigned)CHECK_SPILL_NONE); return gtNewLclvNode(tmp, TYP_I_IMPL); } @@ -20851,6 +20879,12 @@ void Compiler::addGuardedDevirtualizationCandidate(GenTreeCall* call, call->gtGuardedDevirtualizationCandidateInfo = pInfo; } +void Compiler::addExpRuntimeLookupCandidate(GenTreeCall* call) +{ + setMethodHasExpRuntimeLookup(); + call->SetExpRuntimeLookup(); +} + //------------------------------------------------------------------------ // impIsClassExact: check if a class handle can only describe values // of exactly one class. diff --git a/src/coreclr/src/jit/indirectcalltransformer.cpp b/src/coreclr/src/jit/indirectcalltransformer.cpp index 20f2abf0559ac..caec347798031 100644 --- a/src/coreclr/src/jit/indirectcalltransformer.cpp +++ b/src/coreclr/src/jit/indirectcalltransformer.cpp @@ -8,8 +8,8 @@ #endif // The IndirectCallTransformer transforms indirect calls that involve fat function -// pointers or guarded devirtualization candidates. These transformations introduce -// control flow and so can't easily be done in the importer. +// pointers, guarded devirtualization candidates, or runtime lookup with dynamic dictionary expansion. +// These transformations introduce control flow and so can't easily be done in the importer. // // A fat function pointer is a pointer with the second least significant bit // (aka FAT_POINTER_MASK) set. If the bit is set, the pointer (after clearing the bit) @@ -103,19 +103,25 @@ class IndirectCallTransformer for (Statement* stmt : block->Statements()) { - if (ContainsFatCalli(stmt)) + if (compiler->doesMethodHaveFatPointer() && ContainsFatCalli(stmt)) { FatPointerCallTransformer transformer(compiler, block, stmt); transformer.Run(); count++; } - - if (ContainsGuardedDevirtualizationCandidate(stmt)) + else if (compiler->doesMethodHaveGuardedDevirtualization() && + ContainsGuardedDevirtualizationCandidate(stmt)) { GuardedDevirtualizationTransformer transformer(compiler, block, stmt); transformer.Run(); count++; } + else if (compiler->doesMethodHaveExpRuntimeLookup() && ContainsExpRuntimeLookup(stmt)) + { + ExpRuntimeLookupTransformer transformer(compiler, block, stmt); + transformer.Run(); + count++; + } } return count; @@ -154,6 +160,28 @@ class IndirectCallTransformer return candidate->IsCall() && candidate->AsCall()->IsGuardedDevirtualizationCandidate(); } + //------------------------------------------------------------------------ + // ContainsExpRuntimeLookup: check if this statement contains a dictionary + // with dynamic dictionary expansion that we want to transform in CFG. + // + // Return Value: + // true if contains, false otherwise. + // + bool ContainsExpRuntimeLookup(Statement* stmt) + { + GenTree* candidate = stmt->GetRootNode(); + if (candidate->OperIs(GT_ASG)) + { + candidate = candidate->gtGetOp2(); + } + if (candidate->OperIs(GT_CALL)) + { + GenTreeCall* call = candidate->AsCall(); + return call->IsExpRuntimeLookup(); + } + return false; + } + class Transformer { public: @@ -243,7 +271,7 @@ class IndirectCallTransformer //------------------------------------------------------------------------ // SetWeights: set weights for new blocks. // - void SetWeights() + virtual void SetWeights() { remainderBlock->inheritWeight(currBlock); checkBlock->inheritWeight(currBlock); @@ -254,7 +282,7 @@ class IndirectCallTransformer //------------------------------------------------------------------------ // ChainFlow: link new blocks into correct cfg. // - void ChainFlow() + virtual void ChainFlow() { assert(!compiler->fgComputePredsDone); checkBlock->bbJumpDest = elseBlock; @@ -767,6 +795,178 @@ class IndirectCallTransformer unsigned returnTemp; }; + // Runtime lookup with dynamic dictionary expansion transformer, + // it expects helper runtime lookup call with additional arguments that are: + // result handle, nullCheck tree, sizeCheck tree. + // before: + // current block + // { + // previous statements + // transforming statement + // { + // ASG lclVar, call with GTF_CALL_M_EXP_RUNTIME_LOOKUP flag set and additional arguments. + // } + // subsequent statements + // } + // + // after: + // current block + // { + // previous statements + // } BBJ_NONE check block + // check block + // { + // jump to else if the handle fails size check + // } BBJ_COND check block2, else block + // check block2 + // { + // jump to else if the handle fails null check + // } BBJ_COND then block, else block + // then block + // { + // return handle + // } BBJ_ALWAYS remainder block + // else block + // { + // do a helper call + // } BBJ_NONE remainder block + // remainder block + // { + // subsequent statements + // } + // + class ExpRuntimeLookupTransformer final : public Transformer + { + public: + ExpRuntimeLookupTransformer(Compiler* compiler, BasicBlock* block, Statement* stmt) + : Transformer(compiler, block, stmt) + { + GenTreeOp* asg = stmt->GetRootNode()->AsOp(); + resultLclNum = asg->gtOp1->AsLclVar()->GetLclNum(); + origCall = GetCall(stmt); + checkBlock2 = nullptr; + } + + protected: + virtual const char* Name() override + { + return "ExpRuntimeLookup"; + } + + //------------------------------------------------------------------------ + // GetCall: find a call in a statement. + // + // Arguments: + // callStmt - the statement with the call inside. + // + // Return Value: + // call tree node pointer. + virtual GenTreeCall* GetCall(Statement* callStmt) override + { + GenTree* tree = callStmt->GetRootNode(); + assert(tree->OperIs(GT_ASG)); + GenTreeOp* asg = tree->AsOp(); + GenTreeCall* call = tree->gtGetOp2()->AsCall(); + return call; + } + + //------------------------------------------------------------------------ + // ClearFlag: clear runtime exp lookup flag from the original call. + // + virtual void ClearFlag() override + { + origCall->ClearExpRuntimeLookup(); + } + + // FixupRetExpr: no action needed. + virtual void FixupRetExpr() override + { + } + + //------------------------------------------------------------------------ + // CreateCheck: create check blocks, that checks dictionary size and does null test. + // + virtual void CreateCheck() override + { + GenTreeCall::UseIterator argsIter = origCall->Args().begin(); + GenTree* nullCheck = argsIter.GetUse()->GetNode(); + ++argsIter; + GenTree* sizeCheck = argsIter.GetUse()->GetNode(); + ++argsIter; + origCall->gtCallArgs = argsIter.GetUse(); + // The first argument is the handle now. + checkBlock = CreateAndInsertBasicBlock(BBJ_COND, currBlock); + + assert(sizeCheck->OperIs(GT_LE)); + GenTree* sizeJmpTree = compiler->gtNewOperNode(GT_JTRUE, TYP_VOID, sizeCheck); + Statement* sizeJmpStmt = compiler->fgNewStmtFromTree(sizeJmpTree, stmt->GetILOffsetX()); + compiler->fgInsertStmtAtEnd(checkBlock, sizeJmpStmt); + + checkBlock2 = CreateAndInsertBasicBlock(BBJ_COND, checkBlock); + assert(nullCheck->OperIs(GT_EQ)); + GenTree* nullJmpTree = compiler->gtNewOperNode(GT_JTRUE, TYP_VOID, nullCheck); + Statement* nullJmpStmt = compiler->fgNewStmtFromTree(nullJmpTree, stmt->GetILOffsetX()); + compiler->fgInsertStmtAtEnd(checkBlock2, nullJmpStmt); + } + + //------------------------------------------------------------------------ + // CreateThen: create then block, that is executed if the checks succeed. + // This simply returns the handle. + // + virtual void CreateThen() override + { + thenBlock = CreateAndInsertBasicBlock(BBJ_ALWAYS, checkBlock2); + + GenTreeCall::UseIterator argsIter = origCall->Args().begin(); + GenTree* resultHandle = argsIter.GetUse()->GetNode(); + ++argsIter; + // The first argument is the real first argument for the call now. + origCall->gtCallArgs = argsIter.GetUse(); + + GenTree* asg = compiler->gtNewTempAssign(resultLclNum, resultHandle); + Statement* asgStmt = compiler->gtNewStmt(asg, stmt->GetILOffsetX()); + compiler->fgInsertStmtAtEnd(thenBlock, asgStmt); + } + + //------------------------------------------------------------------------ + // CreateElse: create else block, that is executed if the checks fail. + // + virtual void CreateElse() override + { + elseBlock = CreateAndInsertBasicBlock(BBJ_NONE, thenBlock); + GenTree* asg = compiler->gtNewTempAssign(resultLclNum, origCall); + Statement* asgStmt = compiler->gtNewStmt(asg, stmt->GetILOffsetX()); + compiler->fgInsertStmtAtEnd(elseBlock, asgStmt); + } + + //------------------------------------------------------------------------ + // SetWeights: set weights for new blocks. + // + virtual void SetWeights() override + { + remainderBlock->inheritWeight(currBlock); + checkBlock->inheritWeight(currBlock); + checkBlock2->inheritWeightPercentage(checkBlock, HIGH_PROBABILITY); + thenBlock->inheritWeightPercentage(currBlock, HIGH_PROBABILITY); + elseBlock->inheritWeightPercentage(currBlock, 100 - HIGH_PROBABILITY); + } + + //------------------------------------------------------------------------ + // ChainFlow: link new blocks into correct cfg. + // + virtual void ChainFlow() override + { + assert(!compiler->fgComputePredsDone); + checkBlock->bbJumpDest = elseBlock; + checkBlock2->bbJumpDest = elseBlock; + thenBlock->bbJumpDest = remainderBlock; + } + + private: + BasicBlock* checkBlock2; + unsigned resultLclNum; + }; + Compiler* compiler; }; @@ -782,8 +982,10 @@ Compiler::fgWalkResult Compiler::fgDebugCheckForTransformableIndirectCalls(GenTr GenTree* tree = *pTree; if (tree->IsCall()) { - assert(!tree->AsCall()->IsFatPointerCandidate()); - assert(!tree->AsCall()->IsGuardedDevirtualizationCandidate()); + GenTreeCall* call = tree->AsCall(); + assert(!call->IsFatPointerCandidate()); + assert(!call->IsGuardedDevirtualizationCandidate()); + assert(!call->IsExpRuntimeLookup()); } return WALK_CONTINUE; } @@ -796,6 +998,7 @@ void Compiler::CheckNoTransformableIndirectCallsRemain() { assert(!doesMethodHaveFatPointer()); assert(!doesMethodHaveGuardedDevirtualization()); + assert(!doesMethodHaveExpRuntimeLookup()); for (BasicBlock* block = fgFirstBB; block != nullptr; block = block->bbNext) { @@ -817,7 +1020,7 @@ void Compiler::fgTransformIndirectCalls() { JITDUMP("\n*************** in fgTransformIndirectCalls(%s)\n", compIsForInlining() ? "inlinee" : "root"); - if (doesMethodHaveFatPointer() || doesMethodHaveGuardedDevirtualization()) + if (doesMethodHaveFatPointer() || doesMethodHaveGuardedDevirtualization() || doesMethodHaveExpRuntimeLookup()) { IndirectCallTransformer indirectCallTransformer(this); int count = indirectCallTransformer.Run(); @@ -834,6 +1037,7 @@ void Compiler::fgTransformIndirectCalls() clearMethodHasFatPointer(); clearMethodHasGuardedDevirtualization(); + clearMethodHasExpRuntimeLookup(); } else {