Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fold "cast(cast(obj, cls), cls)" to "cast(obj, cls)" #98337

Merged
merged 14 commits into from
Feb 15, 2024
144 changes: 112 additions & 32 deletions src/coreclr/jit/assertionprop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2664,7 +2664,103 @@ AssertionIndex Compiler::optAssertionIsSubtype(GenTree* tree, GenTree* methodTab
}

//------------------------------------------------------------------------------
// optVNConstantPropOnTree: Substitutes tree with an evaluated constant while
// optVNBasedFoldExpr_Call: Folds given call using VN to a simpler tree.
//
// Arguments:
// block - The block containing the tree.
// parent - The parent node of the tree.
// call - The call to fold
//
// Return Value:
// Returns a new tree or nullptr if nothing is changed.
//
GenTree* Compiler::optVNBasedFoldExpr_Call(BasicBlock* block, GenTree* parent, GenTreeCall* call)
{
switch (call->GetHelperNum())
{
//
// Fold "CAST(CAST(obj, cls), cls)" to "CAST(obj, cls)"
// where CAST is either ISINST or CASTCLASS.
//
case CORINFO_HELP_CHKCASTARRAY:
case CORINFO_HELP_CHKCASTANY:
case CORINFO_HELP_CHKCASTINTERFACE:
case CORINFO_HELP_CHKCASTCLASS:
case CORINFO_HELP_ISINSTANCEOFARRAY:
case CORINFO_HELP_ISINSTANCEOFCLASS:
case CORINFO_HELP_ISINSTANCEOFANY:
case CORINFO_HELP_ISINSTANCEOFINTERFACE:
{
GenTree* castClsArg = call->gtArgs.GetUserArgByIndex(0)->GetNode();
GenTree* castObjArg = call->gtArgs.GetUserArgByIndex(1)->GetNode();
ValueNum castClsArgVN = castClsArg->gtVNPair.GetConservative();
ValueNum castObjArgVN = castObjArg->gtVNPair.GetConservative();

if ((castObjArg->gtFlags & GTF_ALL_EFFECT) != 0)
{
// It won't be trivial to properly extract side-effects from the call node.
// Ideally, we only need side effects from the castClsArg argument as the call itself
// won't throw any exceptions. But we should not forget about the EarlyNode (setup args)
return nullptr;
}

VNFuncApp funcApp;
if (vnStore->GetVNFunc(castObjArgVN, &funcApp) &&
((funcApp.m_func == VNF_CastClass) || (funcApp.m_func == VNF_IsInstanceOf)))
EgorBo marked this conversation as resolved.
Show resolved Hide resolved
{
ValueNum innerCastClsVN = funcApp.m_args[0];
if (innerCastClsVN == castClsArgVN)
{
// The outer cast is redundant, remove it and preserve its side effects
// We do ignoreRoot here because the actual cast node is proven to never throw any exceptions.
return gtWrapWithSideEffects(castObjArg, call, GTF_ALL_EFFECT, true);
}
}
}
break;

default:
break;
}

return nullptr;
}

//------------------------------------------------------------------------------
// optVNBasedFoldExpr: Folds given tree using VN to a constant or a simpler tree.
//
// Arguments:
// block - The block containing the tree.
// parent - The parent node of the tree.
// tree - The tree to fold.
//
// Return Value:
// Returns a new tree or nullptr if nothing is changed.
//
GenTree* Compiler::optVNBasedFoldExpr(BasicBlock* block, GenTree* parent, GenTree* tree)
{
// First, attempt to fold it to a constant if possible.
GenTree* foldedToCns = optVNBasedFoldConstExpr(block, parent, tree);
if (foldedToCns != nullptr)
{
return foldedToCns;
}

switch (tree->OperGet())
{
case GT_CALL:
return optVNBasedFoldExpr_Call(block, parent, tree->AsCall());

// We can add more VN-based foldings here.

default:
break;
}
return nullptr;
}

//------------------------------------------------------------------------------
// optVNBasedFoldConstExpr: Substitutes tree with an evaluated constant while
// managing side-effects.
//
// Arguments:
Expand All @@ -2691,7 +2787,7 @@ AssertionIndex Compiler::optAssertionIsSubtype(GenTree* tree, GenTree* methodTab
// the relop will evaluate to "true" or "false" statically, then the side-effects
// will be put into new statements, presuming the JTrue will be folded away.
//
GenTree* Compiler::optVNConstantPropOnTree(BasicBlock* block, GenTree* parent, GenTree* tree)
GenTree* Compiler::optVNBasedFoldConstExpr(BasicBlock* block, GenTree* parent, GenTree* tree)
{
if (tree->OperGet() == GT_JTRUE)
{
Expand Down Expand Up @@ -5109,21 +5205,10 @@ GenTree* Compiler::optAssertionProp_Call(ASSERT_VALARG_TP assertions, GenTreeCal
unsigned index = optAssertionIsSubtype(arg1, arg2, assertions);
if (index != NO_ASSERTION_INDEX)
{
#ifdef DEBUG
if (verbose)
{
printf("\nDid VN based subtype prop for index #%02u in " FMT_BB ":\n", index, compCurBB->bbNum);
gtDispTree(call, nullptr, nullptr, true);
}
#endif
GenTree* list = nullptr;
gtExtractSideEffList(call, &list, GTF_SIDE_EFFECT, true);
if (list != nullptr)
{
arg1 = gtNewOperNode(GT_COMMA, call->TypeGet(), list, arg1);
fgSetTreeSeq(arg1);
}
JITDUMP("\nDid VN based subtype prop for index #%02u in " FMT_BB ":\n", index, compCurBB->bbNum);
DISPTREE(call);

arg1 = gtWrapWithSideEffects(arg1, call, GTF_SIDE_EFFECT, true);
return optAssertionProp_Update(arg1, call, stmt);
}

Expand Down Expand Up @@ -6301,8 +6386,8 @@ GenTree* Compiler::optVNConstantPropOnJTrue(BasicBlock* block, GenTree* test)
}

//------------------------------------------------------------------------------
// optVNConstantPropCurStmt
// Performs constant prop on the current statement's tree nodes.
// optVNBasedFoldCurStmt: Performs VN-based folding
// on the current statement's tree nodes using VN.
//
// Assumption:
// This function is called as part of a post-order tree walk.
Expand All @@ -6316,17 +6401,12 @@ GenTree* Compiler::optVNConstantPropOnJTrue(BasicBlock* block, GenTree* test)
// Return Value:
// Returns the standard visitor walk result.
//
// Description:
// Checks if a node is an R-value and evaluates to a constant. If the node
// evaluates to constant, then the tree is replaced by its side effects and
// the constant node.
//
Compiler::fgWalkResult Compiler::optVNConstantPropCurStmt(BasicBlock* block,
Statement* stmt,
GenTree* parent,
GenTree* tree)
Compiler::fgWalkResult Compiler::optVNBasedFoldCurStmt(BasicBlock* block,
Statement* stmt,
GenTree* parent,
GenTree* tree)
{
// Don't perform const prop on expressions marked with GTF_DONT_CSE
// Don't try and fold expressions marked with GTF_DONT_CSE
// TODO-ASG: delete.
if (!tree->CanCSE())
{
Expand Down Expand Up @@ -6414,8 +6494,8 @@ Compiler::fgWalkResult Compiler::optVNConstantPropCurStmt(BasicBlock* block,
return WALK_CONTINUE;
}

// Perform the constant propagation
GenTree* newTree = optVNConstantPropOnTree(block, parent, tree);
// Perform the VN-based folding:
GenTree* newTree = optVNBasedFoldExpr(block, parent, tree);

if (newTree == nullptr)
{
Expand All @@ -6428,7 +6508,7 @@ Compiler::fgWalkResult Compiler::optVNConstantPropCurStmt(BasicBlock* block,

optAssertionProp_Update(newTree, tree, stmt);

JITDUMP("After constant propagation on [%06u]:\n", tree->gtTreeID);
JITDUMP("After VN-based fold of [%06u]:\n", tree->gtTreeID);
DBEXEC(VERBOSE, gtDispStmt(stmt));

return WALK_CONTINUE;
Expand Down Expand Up @@ -6496,7 +6576,7 @@ Compiler::fgWalkResult Compiler::optVNAssertionPropCurStmtVisitor(GenTree** ppTr

pThis->optVnNonNullPropCurStmt(pData->block, pData->stmt, *ppTree);

return pThis->optVNConstantPropCurStmt(pData->block, pData->stmt, data->parent, *ppTree);
return pThis->optVNBasedFoldCurStmt(pData->block, pData->stmt, data->parent, *ppTree);
}

/*****************************************************************************
Expand Down
11 changes: 9 additions & 2 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -3515,6 +3515,11 @@ class Compiler
GenTreeFlags GenTreeFlags = GTF_SIDE_EFFECT,
bool ignoreRoot = false);

GenTree* gtWrapWithSideEffects(GenTree* tree,
GenTree* sideEffectsSource,
GenTreeFlags sideEffectsFlags = GTF_SIDE_EFFECT,
bool ignoreRoot = false);

bool gtSplitTree(
BasicBlock* block, Statement* stmt, GenTree* splitPoint, Statement** firstNewStmt, GenTree*** splitPointUse);

Expand Down Expand Up @@ -7710,9 +7715,11 @@ class Compiler

public:
void optVnNonNullPropCurStmt(BasicBlock* block, Statement* stmt, GenTree* tree);
fgWalkResult optVNConstantPropCurStmt(BasicBlock* block, Statement* stmt, GenTree* parent, GenTree* tree);
fgWalkResult optVNBasedFoldCurStmt(BasicBlock* block, Statement* stmt, GenTree* parent, GenTree* tree);
GenTree* optVNConstantPropOnJTrue(BasicBlock* block, GenTree* test);
GenTree* optVNConstantPropOnTree(BasicBlock* block, GenTree* parent, GenTree* tree);
GenTree* optVNBasedFoldConstExpr(BasicBlock* block, GenTree* parent, GenTree* tree);
GenTree* optVNBasedFoldExpr(BasicBlock* block, GenTree* parent, GenTree* tree);
GenTree* optVNBasedFoldExpr_Call(BasicBlock* block, GenTree* parent, GenTreeCall* call);
GenTree* optExtractSideEffListFromConst(GenTree* tree);

AssertionIndex GetAssertionCount()
Expand Down
38 changes: 38 additions & 0 deletions src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17063,6 +17063,44 @@ bool Compiler::gtSplitTree(
return splitter.MadeChanges;
}

//------------------------------------------------------------------------
// gtWrapWithSideEffects: Extracts side effects from sideEffectSource (if any)
// and wraps the input tree with a COMMA node with them.
//
// Arguments:
// tree - the expression tree to wrap with side effects (if any)
// it has to be either a side effect free subnode of sideEffectsSource
// or any tree outside sideEffectsSource's hierarchy
// sideEffectsSource - the expression tree to extract side effects from
// sideEffectsFlags - side effect flags to be considered
// ignoreRoot - ignore side effects on the expression root node
//
// Return Value:
// The original tree wrapped with a COMMA node that contains the side effects
// or just the tree itself if sideEffectSource has no side effects.
//
GenTree* Compiler::gtWrapWithSideEffects(GenTree* tree,
Copy link
Member

Choose a reason for hiding this comment

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

Seems like commonly tree is a subtree of sideEffectsSource.

If tree itself has side effects, do those get handled properly?

Copy link
Member Author

@EgorBo EgorBo Feb 14, 2024

Choose a reason for hiding this comment

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

That is a good point, added a comment. Technically, it should be either a sideffect-free subtree of sideEffectsSource or pretty much any node with any sideffects if it's not a subtree. In two uses of this API where tree is a subnode of sideEffectsSource we do check it being side-effect free at the callsite. It's probably possible to be smarter here and extract the effects properly for any case, but not now. Setup-arg stuff in args makes it a bit more complicated 🙁

GenTree* sideEffectsSource,
GenTreeFlags sideEffectsFlags,
bool ignoreRoot)
{
GenTree* sideEffects = nullptr;
gtExtractSideEffList(sideEffectsSource, &sideEffects, sideEffectsFlags, ignoreRoot);
if (sideEffects != nullptr)
{
// TODO: assert if tree is a subnode of sideEffectsSource and the tree has its own side effects
// otherwise the resulting COMMA might have some side effects to be duplicated
// For now, caller is responsible for ensuring that will not happen.

tree = gtNewOperNode(GT_COMMA, tree->TypeGet(), sideEffects, tree);
if (fgNodeThreading == NodeThreading::AllTrees)
{
fgSetTreeSeq(tree);
}
EgorBo marked this conversation as resolved.
Show resolved Hide resolved
}
return tree;
}

//------------------------------------------------------------------------
// gtExtractSideEffList: Extracts side effects from the given expression.
//
Expand Down
3 changes: 1 addition & 2 deletions src/coreclr/jit/importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1677,8 +1677,7 @@ GenTree* Compiler::impRuntimeLookupToTree(CORINFO_RESOLVED_TOKEN* pResolvedToken
return slotPtrTree;
}

slotPtrTree = gtNewIndir(TYP_I_IMPL, slotPtrTree, GTF_IND_NONFAULTING);
slotPtrTree->gtFlags &= ~GTF_GLOB_REF; // TODO-Bug?: this is a quirk. Can we mark this indirection invariant?
slotPtrTree = gtNewIndir(TYP_I_IMPL, slotPtrTree, GTF_IND_NONFAULTING | GTF_IND_INVARIANT);
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see why it can't be invariant (and removed the GTF_GLOB_REF quirk). However, I most likely will have to unmark it as invariant in my "inlining for shared generics" work for certain cases, but it's irrelevant here.

Copy link
Member Author

@EgorBo EgorBo Feb 13, 2024

Choose a reason for hiding this comment

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

It was done for my snippet in the PR's description, otherwise two exactly the same runtime lookups had different conservative VNs.

Copy link
Member

Choose a reason for hiding this comment

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

I would double-check this with somebody on the runtime team; getting this wrong can have subtle consequences.

Copy link
Member Author

@EgorBo EgorBo Feb 14, 2024

Choose a reason for hiding this comment

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

So from my understanding there are two types of runtime lookups:

  1. Caller-side
  2. Callee-side

The caller side is typically not invariant (at least in JIT) and requires a helper call + expansion machinery (checking for null/dictionary resize).

So if a runtime lookup doesn't need a helper-call/null check/size check - it's a fully invariant bunch of ind loads on top of TypeCtx.

It might be a bit more complicated if we implement inlining for shared generics where we're going to have both caller and callee lookups in the same method, but I will keep that in mind once I do that sort of inlining (as part of net9). Currently we never have such cases.

cc @jkotas to confirm.

Copy link
Member

Choose a reason for hiding this comment

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

The generic dictionary slots that do not require helper-call/null check/size check can be treated as invariant. This change looks fine to me.

So from my understanding there are two types of runtime lookups:
Caller-side
Callee-side

I am not sure what you mean. There is no distinction like this that I am aware of.

Copy link
Member Author

@EgorBo EgorBo Feb 14, 2024

Choose a reason for hiding this comment

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

I am not sure what you mean. There is no distinction like this that I am aware of.

Yes, please, ignore that part. There are just 4 types of lookups:

  1. Helper call + nullcheck + INDs (fast path)
  2. Helper call + nullcheck + sizecheck + INDs (fast path)
  3. nullcheck + INDs (fast path)
  4. INDs (fast path)
    where INDs is [0..n] nested indirect loads.

Copy link
Member

Choose a reason for hiding this comment

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

nullcheck + INDs (fast path)

Nit: These do not exist. The null check needs a helper call slow path.


return slotPtrTree;
}
Expand Down
18 changes: 2 additions & 16 deletions src/coreclr/jit/morph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9653,22 +9653,8 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA
}
else
{
GenTree* op1SideEffects = nullptr;
gtExtractSideEffList(op1, &op1SideEffects, GTF_ALL_EFFECT);
if (op1SideEffects != nullptr)
{
DEBUG_DESTROY_NODE(tree);
// Keep side-effects of op1
tree = gtNewOperNode(GT_COMMA, TYP_INT, op1SideEffects, gtNewIconNode(0));
JITDUMP("false with side effects:\n")
DISPTREE(tree);
}
else
{
JITDUMP("false\n");
DEBUG_DESTROY_NODE(tree, op1);
tree = gtNewIconNode(0);
}
JITDUMP("false\n");
tree = gtWrapWithSideEffects(gtNewIconNode(0), op1, GTF_ALL_EFFECT);
}
INDEBUG(tree->gtDebugFlags |= GTF_DEBUG_NODE_MORPHED);
return tree;
Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/jit/valuenum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13098,6 +13098,13 @@ bool Compiler::fgValueNumberHelperCall(GenTreeCall* call)
vnStore->VNPairForFunc(TYP_REF, VNF_OverflowExc, vnStore->VNPForVoid()));
break;

case CORINFO_HELP_CHKCASTINTERFACE:
case CORINFO_HELP_CHKCASTARRAY:
case CORINFO_HELP_CHKCASTCLASS:
case CORINFO_HELP_CHKCASTANY:
// InvalidCastExc for these is set in VNForCast
break;

default:
// Setup vnpExc with the information that multiple different exceptions
// could be generated by this helper
Expand Down
Loading