diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 51c7109aa1dd31..6420edaa09eb11 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4345,6 +4345,8 @@ class Compiler GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass, IL_OFFSET ilOffset); GenTree* impOptimizeCastClassOrIsInst(GenTree* op1, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass); + bool impExtractNullableForCastClassOrIsInst(GenTree** op2, CORINFO_RESOLVED_TOKEN* pResolvedToken); + int impIsInstPatternMatch(GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, const BYTE* codeAddr, const BYTE* codeEndp); bool VarTypeIsMultiByteAndCanEnreg(var_types type, CORINFO_CLASS_HANDLE typeClass, diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 722afadcd9a210..82fe969628564b 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5310,6 +5310,170 @@ GenTree* Compiler::impOptimizeCastClassOrIsInst(GenTree* op1, CORINFO_RESOLVED_T return nullptr; } +//------------------------------------------------------------------------ +// impExtractNullableForCastClassOrIsInst: change nullable to underlying type for casting +// +// Arguments: +// pOp2 - type handle for type to cast to +// pResolvedToken - resolved token from the cast operation +// +// Return Value: +// If the cast can be expanded to comparison of method table to op2. +// +// Notes: +// If op2 is updated, resolvedToken will also be updated correspondingly. + +bool Compiler::impExtractNullableForCastClassOrIsInst(GenTree** pOp2, CORINFO_RESOLVED_TOKEN* pResolvedToken) +{ + // ECMA-335 III.4.3: If typeTok is a nullable type, Nullable, it is interpreted as "boxed" T + // We can convert constant-ish tokens of nullable to its underlying type. + // However, when the type is shared generic parameter like Nullable>, the actual type will require + // runtime lookup. It's too complex to add another level of indirection in op2, fallback to the cast helper instead. + + if (info.compCompHnd->getClassAttribs(pResolvedToken->hClass) & CORINFO_FLG_SHAREDINST) + return false; + + CORINFO_CLASS_HANDLE hClass = info.compCompHnd->getTypeForBox(pResolvedToken->hClass); + + if (hClass != pResolvedToken->hClass) + { + bool runtimeLookup; + pResolvedToken->hClass = hClass; + *pOp2 = impTokenToHandle(pResolvedToken, &runtimeLookup); + assert(!runtimeLookup); + } + + return true; +} + +//------------------------------------------------------------------------ +// impIsInstPatternMatch: match and import common isinst idioms +// +// Arguments: +// op1 - value to cast +// op2 - type handle for type to cast to +// codeAddr - position in IL stream after the box instruction +// codeEndp - end of IL stream +// +// Return Value: +// Number of IL bytes matched and imported, -1 otherwise +// +// Notes: +// op2 is known to represent an exact type (sealed or value type). + +int Compiler::impIsInstPatternMatch( + GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, const BYTE* codeAddr, const BYTE* codeEndp) +{ + int imported = -1; + bool isNotNullTest = false; + + if (!impIsClassExact(pResolvedToken->hClass) || !impExtractNullableForCastClassOrIsInst(&op2, pResolvedToken)) + return -1; + + switch (impGetNonPrefixOpcode(codeAddr, codeEndp)) + { + case CEE_LDNULL: + switch (impGetNonPrefixOpcode(codeAddr + 1, codeEndp)) + { + case CEE_CGT_UN: + // isinst + ldnull + cgt.un + imported = 3; + isNotNullTest = true; + break; + + case CEE_CEQ: + // isinst + ldnull + ceq + imported = 3; + isNotNullTest = false; + break; + + default: + return -1; + } + break; + + case CEE_BRTRUE: + case CEE_BRTRUE_S: + // isinst + brtrue + // leave the branch IL + imported = 0; + isNotNullTest = true; + break; + + case CEE_BRFALSE: + case CEE_BRFALSE_S: + // isinst + false + // leave the branch IL + imported = 0; + isNotNullTest = false; + break; + + default: + return -1; + } + + // Converting isinst followed by null test to: + // non-null test: op1 == null ? false : *op1 == op2 + // null test: op1 == null ? true : *op1 != op2 + + GenTree* temp; + GenTree* condMT; + // + // expand the methodtable match: + // + // condMT ==> GT_EQ/NE + // / \. + // GT_IND op2 (typically CNS_INT) + // | + // op1Copy + // + + // This can replace op1 with a GT_COMMA that evaluates op1 into a local + // + op1 = impCloneExpr(op1, &temp, CHECK_SPILL_ALL, nullptr DEBUGARG("CASTCLASS eval op1")); + // + // op1 is now known to be a non-complex tree + // thus we can use gtClone(op1) from now on + // + + GenTree* op2Var = op2; + temp = gtNewMethodTableLookup(temp); + condMT = gtNewOperNode(isNotNullTest ? GT_EQ : GT_NE, TYP_INT, temp, op2); + + GenTree* condNull; + // + // expand the null check: + // + // condNull ==> GT_EQ + // / \. + // op1Copy CNS_INT + // null + // + condNull = gtNewOperNode(GT_EQ, TYP_INT, gtClone(op1), gtNewNull()); + + GenTree* qmarkNull; + // + // Generate QMARK - COLON tree + // + // qmarkNull ==> GT_QMARK + // / \. + // condNull GT_COLON + // / \. + // CNS_INT condMT + // false/true + // + temp = gtNewColonNode(TYP_INT, isNotNullTest ? gtNewFalse() : gtNewTrue(), condMT); + qmarkNull = gtNewQmarkNode(TYP_INT, condNull, temp->AsColon()); + + // Make QMark node a top level node by spilling it. + unsigned tmp = lvaGrabTemp(true DEBUGARG("spilling QMark")); + impStoreTemp(tmp, qmarkNull, CHECK_SPILL_NONE); + + GenTree* lclVar = gtNewLclvNode(tmp, TYP_INT); + impPushOnStack(lclVar, TYP_INT); + return imported; +} + //------------------------------------------------------------------------ // impCastClassOrIsInstToTree: build and import castclass/isinst // @@ -5334,21 +5498,10 @@ GenTree* Compiler::impCastClassOrIsInstToTree( bool shouldExpandInline = true; bool isClassExact = impIsClassExact(pResolvedToken->hClass); - // ECMA-335 III.4.3: If typeTok is a nullable type, Nullable, it is interpreted as "boxed" T - // We can convert constant-ish tokens of nullable to its underlying type. - // However, when the type is shared generic parameter like Nullable>, the actual type will require - // runtime lookup. It's too complex to add another level of indirection in op2, fallback to the cast helper instead. - if (isClassExact && !(info.compCompHnd->getClassAttribs(pResolvedToken->hClass) & CORINFO_FLG_SHAREDINST)) + if (isClassExact) { - CORINFO_CLASS_HANDLE hClass = info.compCompHnd->getTypeForBox(pResolvedToken->hClass); - - if (hClass != pResolvedToken->hClass) - { - bool runtimeLookup; - pResolvedToken->hClass = hClass; - op2 = impTokenToHandle(pResolvedToken, &runtimeLookup); - assert(!runtimeLookup); - } + // Convert nullable to underlying type when appropriate. + impExtractNullableForCastClassOrIsInst(&op2, pResolvedToken); } // Profitability check. @@ -9587,6 +9740,16 @@ void Compiler::impImportBlockCode(BasicBlock* block) if (!usingReadyToRunHelper) #endif { + // try to fold null check after exact isinst to MT comparison first + int matched = impIsInstPatternMatch(op1, op2, &resolvedToken, codeAddr + sz, codeEndp); + + if (matched >= 0) + { + // Skip the matched IL instructions + sz += matched; + break; + } + op1 = impCastClassOrIsInstToTree(op1, op2, &resolvedToken, false, opcodeOffs); } if (compDonotInline())