Skip to content

Commit

Permalink
JIT: Produce special cased IR for boolean isinst checks (#103391)
Browse files Browse the repository at this point in the history
Currently the IR for boolean `isinst` checks ends up being something like `(x !=
null ? x.mt == expectedMT ? x : null : null) != null`, which the JIT ends up
having a hard time clean up early. With object stack allocation this pattern
usually leads to unnecessary address exposure.

This adds a simple pattern match during import to produce different IR in the
common cases where the `isinst` is just used as a boolean check. We instead
produce IR like `(x != null ? x.mt == expectedMT ? 1 : 0 : 0) != 0`, which the
JIT has an easier time with.
  • Loading branch information
jakobbotsch authored Jun 18, 2024
1 parent cb19811 commit b2ccd98
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 16 deletions.
4 changes: 3 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4777,8 +4777,10 @@ class Compiler
bool impIsCastHelperEligibleForClassProbe(GenTree* tree);
bool impIsCastHelperMayHaveProfileData(CorInfoHelpFunc helper);

bool impMatchIsInstBooleanConversion(const BYTE* codeAddr, const BYTE* codeEndp, int* consumed);

GenTree* impCastClassOrIsInstToTree(
GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass, IL_OFFSET ilOffset);
GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass, bool* booleanCheck, IL_OFFSET ilOffset);

GenTree* impOptimizeCastClassOrIsInst(GenTree* op1, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass);

Expand Down
112 changes: 97 additions & 15 deletions src/coreclr/jit/importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5468,6 +5468,53 @@ GenTree* Compiler::impOptimizeCastClassOrIsInst(GenTree* op1, CORINFO_RESOLVED_T
return nullptr;
}

//------------------------------------------------------------------------
// impMatchIsInstBooleanConversion: Match IL to determine whether an isinst IL
// instruction is used for a simple boolean check.
//
// Arguments:
// codeAddr - IL after the isinst
// codeEndp - End of IL code stream
// consumed - [out] If this function returns true, set to the number of IL
// bytes to consume to create the boolean check
//
// Return Value:
// True if the isinst is used as a boolean check; otherwise false.
//
// Remarks:
// The isinst instruction is specced to return the original object refernce
// when the type check succeeds. However, in many cases it is used strictly
// as a boolean type check (if (x is Foo) for example). In those cases it is
// beneficial for the JIT if we avoid creating QMARKs returning the object
// itself which may disable some important optimization in some cases.
//
bool Compiler::impMatchIsInstBooleanConversion(const BYTE* codeAddr, const BYTE* codeEndp, int* consumed)
{
OPCODE nextOpcode = impGetNonPrefixOpcode(codeAddr, codeEndp);
switch (nextOpcode)
{
case CEE_BRFALSE:
case CEE_BRFALSE_S:
case CEE_BRTRUE:
case CEE_BRTRUE_S:
// BRFALSE/BRTRUE importation are expected to transparently handle
// that the created tree is a TYP_INT instead of TYP_REF, so we do
// not consume them here.
*consumed = 0;
return true;
case CEE_LDNULL:
nextOpcode = impGetNonPrefixOpcode(codeAddr + 1, codeEndp);
if (nextOpcode == CEE_CGT_UN)
{
*consumed = 3;
return true;
}
return false;
default:
return false;
}
}

//------------------------------------------------------------------------
// impCastClassOrIsInstToTree: build and import castclass/isinst
//
Expand All @@ -5476,15 +5523,22 @@ GenTree* Compiler::impOptimizeCastClassOrIsInst(GenTree* op1, CORINFO_RESOLVED_T
// op2 - type handle for type to cast to
// pResolvedToken - resolved token from the cast operation
// isCastClass - true if this is castclass, false means isinst
// booleanCheck - [in, out] If true, allow creating a boolean-returning check
// instead of returning the object reference. Set to false if this function
// was not able to create a boolean check.
//
// Return Value:
// Tree representing the cast
//
// Notes:
// May expand into a series of runtime checks or a helper call.
//
GenTree* Compiler::impCastClassOrIsInstToTree(
GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass, IL_OFFSET ilOffset)
GenTree* Compiler::impCastClassOrIsInstToTree(GenTree* op1,
GenTree* op2,
CORINFO_RESOLVED_TOKEN* pResolvedToken,
bool isCastClass,
bool* booleanCheck,
IL_OFFSET ilOffset)
{
assert(op1->TypeGet() == TYP_REF);

Expand Down Expand Up @@ -5557,6 +5611,8 @@ GenTree* Compiler::impCastClassOrIsInstToTree(
call->gtCallMoreFlags |= GTF_CALL_M_CAST_CAN_BE_EXPANDED;
call->gtCastHelperILOffset = ilOffset;
}

*booleanCheck = false;
return call;
}

Expand All @@ -5567,34 +5623,52 @@ GenTree* Compiler::impCastClassOrIsInstToTree(
// Now we import it as two QMark nodes representing this:
//
// tmp = op1;
// if (tmp != null) // qmarkNull
// if (tmp != null) // condNull
// {
// if (tmp->pMT == op2) // qmarkMT
// if (tmp->pMT == op2) // condMT
// result = tmp;
// else
// result = null;
// }
// else
// result = null;
//
// When a boolean check is possible we create 1/0 instead of tmp/null.

// Spill op1 if it's a complex expression
GenTree* op1Clone;
op1 = impCloneExpr(op1, &op1Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("ISINST eval op1"));

GenTreeOp* condMT = gtNewOperNode(GT_NE, TYP_INT, gtNewMethodTableLookup(op1Clone), op2);
GenTreeOp* condNull = gtNewOperNode(GT_EQ, TYP_INT, gtClone(op1), gtNewNull());
GenTreeQmark* qmarkMT = gtNewQmarkNode(TYP_REF, condMT, gtNewColonNode(TYP_REF, gtNewNull(), gtClone(op1)));
GenTreeQmark* qmarkNull = gtNewQmarkNode(TYP_REF, condNull, gtNewColonNode(TYP_REF, gtNewNull(), qmarkMT));
GenTreeOp* condNull = gtNewOperNode(GT_EQ, TYP_INT, gtClone(op1), gtNewNull());
GenTreeOp* condMT = gtNewOperNode(GT_NE, TYP_INT, gtNewMethodTableLookup(op1Clone), op2);

GenTreeQmark* qmarkResult;

if (*booleanCheck)
{
GenTreeQmark* qmarkMT =
gtNewQmarkNode(TYP_INT, condMT,
gtNewColonNode(TYP_INT, gtNewZeroConNode(TYP_INT), gtNewOneConNode(TYP_INT)));
qmarkResult = gtNewQmarkNode(TYP_INT, condNull, gtNewColonNode(TYP_INT, gtNewZeroConNode(TYP_INT), qmarkMT));
}
else
{
GenTreeQmark* qmarkMT = gtNewQmarkNode(TYP_REF, condMT, gtNewColonNode(TYP_REF, gtNewNull(), gtClone(op1)));
qmarkResult = gtNewQmarkNode(TYP_REF, condNull, gtNewColonNode(TYP_REF, gtNewNull(), qmarkMT));
}

// Make QMark node a top level node by spilling it.
const unsigned result = lvaGrabTemp(true DEBUGARG("spilling qmarkNull"));
impStoreToTemp(result, qmarkNull, CHECK_SPILL_NONE);
impStoreToTemp(result, qmarkResult, CHECK_SPILL_NONE);

// See also gtGetHelperCallClassHandle where we make the same
// determination for the helper call variants.
lvaSetClass(result, pResolvedToken->hClass);
return gtNewLclvNode(result, TYP_REF);
if (!*booleanCheck)
{
// See also gtGetHelperCallClassHandle where we make the same
// determination for the helper call variants.
lvaSetClass(result, pResolvedToken->hClass);
}

return gtNewLclvNode(result, qmarkResult->TypeGet());
}

#ifndef DEBUG
Expand Down Expand Up @@ -9630,7 +9704,14 @@ void Compiler::impImportBlockCode(BasicBlock* block)
if (!usingReadyToRunHelper)
#endif
{
op1 = impCastClassOrIsInstToTree(op1, op2, &resolvedToken, false, opcodeOffs);
int consumed = 0;
bool booleanCheck = impMatchIsInstBooleanConversion(codeAddr + sz, codeEndp, &consumed);
op1 = impCastClassOrIsInstToTree(op1, op2, &resolvedToken, false, &booleanCheck, opcodeOffs);

if (booleanCheck)
{
sz += consumed;
}
}
if (compDonotInline())
{
Expand Down Expand Up @@ -10152,7 +10233,8 @@ void Compiler::impImportBlockCode(BasicBlock* block)
if (!usingReadyToRunHelper)
#endif
{
op1 = impCastClassOrIsInstToTree(op1, op2, &resolvedToken, true, opcodeOffs);
bool booleanCheck = false;
op1 = impCastClassOrIsInstToTree(op1, op2, &resolvedToken, true, &booleanCheck, opcodeOffs);
}
if (compDonotInline())
{
Expand Down

0 comments on commit b2ccd98

Please sign in to comment.