Skip to content

Commit 2f63046

Browse files
authored
JIT: Validate loop invariant base case as part of IV analysis (#97122)
Move the validation of the base case of the IV analysis into `FlowGraphNaturalLoop::AnalyzeIteration`. Previously the validation for the base case was left up to the users: - Loop cloning tried to accomplish this by checking whether loop inversion had run, but this is not sufficient (#96623) as nothing guarantees the loop is dominated by the inverted test. Loop cloning also skipped the check entirely for GDV, leading to #95315. - Unrolling completely neglected to check the condition leading to #96591. Furthermore, unrolling made implicit assumptions that any `BBJ_COND` init block was an inverted test and downright removed the condition without any checks. This also led to another issue #97040 where unrolling could uncover new natural loops that had not been canonicalized. This change makes `FlowGraphNaturalLoop::AnalyzeIteration` try to prove that the loop invariant it returns is true in the base case as well as being maintained by the loop. If it cannot prove this then it fails. This fixes all the issues, but sadly uncovers that we were doing a lot of cloning in OSR methods without actually proving legality. We no longer clone in these cases, but we can look into what to do about them separately. Fix #95315 Fix #96591 Fix #96623 Fix #97040
1 parent 3ca0c5a commit 2f63046

File tree

12 files changed

+436
-126
lines changed

12 files changed

+436
-126
lines changed

src/coreclr/jit/compiler.h

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,14 +2006,16 @@ struct NaturalLoopIterInfo
20062006
// The local that is the induction variable.
20072007
unsigned IterVar = BAD_VAR_NUM;
20082008

2009+
#ifdef DEBUG
2010+
// Tree that initializes induction varaible outside the loop.
2011+
// Only valid if HasConstInit is true.
2012+
GenTree* InitTree = nullptr;
2013+
#endif
2014+
20092015
// Constant value that the induction variable is initialized with, outside
20102016
// the loop. Only valid if HasConstInit is true.
20112017
int ConstInitValue = 0;
20122018

2013-
// Block outside the loop that initializes the induction variable. Only
2014-
// valid if HasConstInit is true.
2015-
BasicBlock* InitBlock = nullptr;
2016-
20172019
// Tree that has the loop test for the induction variable.
20182020
GenTree* TestTree = nullptr;
20192021

@@ -2023,6 +2025,9 @@ struct NaturalLoopIterInfo
20232025
// Tree that mutates the induction variable.
20242026
GenTree* IterTree = nullptr;
20252027

2028+
// Is the loop exited when TestTree is true?
2029+
bool ExitedOnTrue : 1;
2030+
20262031
// Whether or not we found an initialization of the induction variable.
20272032
bool HasConstInit : 1;
20282033

@@ -2042,7 +2047,8 @@ struct NaturalLoopIterInfo
20422047
bool HasArrayLengthLimit : 1;
20432048

20442049
NaturalLoopIterInfo()
2045-
: HasConstInit(false)
2050+
: ExitedOnTrue(false)
2051+
, HasConstInit(false)
20462052
, HasConstLimit(false)
20472053
, HasSimdLimit(false)
20482054
, HasInvariantLocalLimit(false)
@@ -2053,7 +2059,6 @@ struct NaturalLoopIterInfo
20532059
int IterConst();
20542060
genTreeOps IterOper();
20552061
var_types IterOperType();
2056-
bool IsReversed();
20572062
genTreeOps TestOper();
20582063
bool IsIncreasingLoop();
20592064
bool IsDecreasingLoop();
@@ -2062,6 +2067,9 @@ struct NaturalLoopIterInfo
20622067
int ConstLimit();
20632068
unsigned VarLimit();
20642069
bool ArrLenLimit(Compiler* comp, ArrIndex* index);
2070+
2071+
private:
2072+
bool IsReversed();
20652073
};
20662074

20672075
// Represents a natural loop in the flow graph. Natural loops are characterized
@@ -2136,7 +2144,9 @@ class FlowGraphNaturalLoop
21362144

21372145
void MatchInit(NaturalLoopIterInfo* info, BasicBlock* initBlock, GenTree* init);
21382146
bool MatchLimit(NaturalLoopIterInfo* info, GenTree* test);
2139-
2147+
bool CheckLoopConditionBaseCase(BasicBlock* initBlock, NaturalLoopIterInfo* info);
2148+
template<typename T>
2149+
static bool EvaluateRelop(T op1, T op2, genTreeOps oper);
21402150
public:
21412151
BasicBlock* GetHeader() const
21422152
{
@@ -7201,7 +7211,6 @@ class Compiler
72017211
var_types iterType,
72027212
genTreeOps testOper,
72037213
bool unsignedTest,
7204-
bool dupCond,
72057214
unsigned* iterCount);
72067215

72077216
protected:

src/coreclr/jit/flowgraph.cpp

Lines changed: 155 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4933,15 +4933,31 @@ GenTreeLclVarCommon* FlowGraphNaturalLoop::FindDef(unsigned lclNum)
49334933
// otherwise false.
49344934
//
49354935
// Remarks:
4936-
// The function guarantees that at all definitions of the iteration local,
4937-
// the loop condition is reestablished before iteration continues. In other
4938-
// words, if this function returns true the loop invariant is guaranteed to
4939-
// be upheld in all blocks in the loop (but see below for establishing the
4940-
// base case).
4941-
//
4942-
// Currently we do not validate that there is a zero-trip test ensuring the
4943-
// condition is true, so it is up to the user to validate that. This is
4944-
// normally done via GTF_RELOP_ZTT set by loop inversion.
4936+
// On a true return, the function guarantees that the loop invariant is true
4937+
// and maintained at all points within the loop, except possibly right after
4938+
// the update of the iterator variable (NaturalLoopIterInfo::IterTree). The
4939+
// function guarantees that the test (NaturalLoopIterInfo::TestTree) occurs
4940+
// immediately after the update, so no IR in the loop is executed without the
4941+
// loop invariant being true, except for the test.
4942+
//
4943+
// The loop invariant is defined as the expression obtained by
4944+
// [info->IterVar] [info->TestOper()] [info->Limit()]. Note that
4945+
// [info->TestTree()] may not be of this form; it could for instance have the
4946+
// iterator variable as the second operand. However,
4947+
// [NaturalLoopIterInfo::TestOper()] will automatically normalize the test
4948+
// oper so that the invariant is equivalent to the returned form that has the
4949+
// iteration variable as op1 and the limit as op2.
4950+
//
4951+
// The limit can be further decomposed via NaturalLoopIterInfo::ConstLimit,
4952+
// ::VarLimit and ::ArrLenLimit.
4953+
//
4954+
// As an example, if info->IterVar == V02, info->TestOper() == GT_LT and
4955+
// info->ConstLimit() == 10, then the function guarantees that the value of
4956+
// the local V02 is less than 10 everywhere within the loop (except possibly
4957+
// at the test).
4958+
//
4959+
// In some cases we also know the initial value on entry to the loop; see
4960+
// ::HasConstInit and ::ConstInitValue.
49454961
//
49464962
bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
49474963
{
@@ -4972,8 +4988,15 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
49724988
return false;
49734989
}
49744990

4975-
info->TestBlock = cond;
4976-
info->IterVar = comp->optIsLoopIncrTree(info->IterTree);
4991+
assert(ContainsBlock(cond->GetTrueTarget()) != ContainsBlock(cond->GetFalseTarget()));
4992+
4993+
info->TestBlock = cond;
4994+
info->IterVar = comp->optIsLoopIncrTree(info->IterTree);
4995+
info->ExitedOnTrue = !ContainsBlock(cond->GetTrueTarget());
4996+
4997+
// TODO-CQ: Currently, since we only look at the lexically bottom most block all loops have
4998+
// ExitedOnTrue == false. Once we recognize more structures this can be true.
4999+
assert(!info->ExitedOnTrue);
49775000

49785001
assert(info->IterVar != BAD_VAR_NUM);
49795002
LclVarDsc* const iterVarDsc = comp->lvaGetDesc(info->IterVar);
@@ -5027,13 +5050,20 @@ bool FlowGraphNaturalLoop::AnalyzeIteration(NaturalLoopIterInfo* info)
50275050
return false;
50285051
}
50295052

5053+
if (!CheckLoopConditionBaseCase(initBlock, info))
5054+
{
5055+
JITDUMP(" Loop condition is not always true\n");
5056+
return false;
5057+
}
5058+
50305059
#ifdef DEBUG
50315060
if (comp->verbose)
50325061
{
50335062
printf(" IterVar = V%02u\n", info->IterVar);
50345063

50355064
if (info->HasConstInit)
5036-
printf(" Const init with value %d in " FMT_BB "\n", info->ConstInitValue, info->InitBlock->bbNum);
5065+
printf(" Const init with value %d (at [%06u])\n", info->ConstInitValue,
5066+
Compiler::dspTreeID(info->InitTree));
50375067

50385068
printf(" Test is [%06u] (", Compiler::dspTreeID(info->TestTree));
50395069
if (info->HasConstLimit)
@@ -5075,7 +5105,7 @@ void FlowGraphNaturalLoop::MatchInit(NaturalLoopIterInfo* info, BasicBlock* init
50755105

50765106
info->HasConstInit = true;
50775107
info->ConstInitValue = (int)initValue->AsIntCon()->IconValue();
5078-
info->InitBlock = initBlock;
5108+
INDEBUG(info->InitTree = init);
50795109
}
50805110

50815111
//------------------------------------------------------------------------
@@ -5118,12 +5148,12 @@ bool FlowGraphNaturalLoop::MatchLimit(NaturalLoopIterInfo* info, GenTree* test)
51185148
GenTree* limitOp;
51195149

51205150
// Make sure op1 or op2 is the iterVar.
5121-
if (opr1->gtOper == GT_LCL_VAR && opr1->AsLclVarCommon()->GetLclNum() == info->IterVar)
5151+
if (opr1->OperIsScalarLocal() && (opr1->AsLclVarCommon()->GetLclNum() == info->IterVar))
51225152
{
51235153
iterOp = opr1;
51245154
limitOp = opr2;
51255155
}
5126-
else if (opr2->gtOper == GT_LCL_VAR && opr2->AsLclVarCommon()->GetLclNum() == info->IterVar)
5156+
else if (opr2->OperIsScalarLocal() && (opr2->AsLclVarCommon()->GetLclNum() == info->IterVar))
51275157
{
51285158
iterOp = opr2;
51295159
limitOp = opr1;
@@ -5138,11 +5168,8 @@ bool FlowGraphNaturalLoop::MatchLimit(NaturalLoopIterInfo* info, GenTree* test)
51385168
return false;
51395169
}
51405170

5141-
// Mark the iterator node.
5142-
iterOp->gtFlags |= GTF_VAR_ITERATOR;
5143-
51445171
// Check what type of limit we have - constant, variable or arr-len.
5145-
if (limitOp->gtOper == GT_CNS_INT)
5172+
if (limitOp->IsCnsIntOrI())
51465173
{
51475174
info->HasConstLimit = true;
51485175
if ((limitOp->gtFlags & GTF_ICON_SIMD_COUNT) != 0)
@@ -5212,6 +5239,105 @@ bool FlowGraphNaturalLoop::MatchLimit(NaturalLoopIterInfo* info, GenTree* test)
52125239
return true;
52135240
}
52145241

5242+
//------------------------------------------------------------------------
5243+
// EvaluateRelop: Evaluate a relational operator with constant arguments.
5244+
//
5245+
// Parameters:
5246+
// op1 - First operand
5247+
// op2 - Second operand
5248+
// oper - Operator
5249+
//
5250+
// Returns:
5251+
// Result.
5252+
//
5253+
template <typename T>
5254+
bool FlowGraphNaturalLoop::EvaluateRelop(T op1, T op2, genTreeOps oper)
5255+
{
5256+
switch (oper)
5257+
{
5258+
case GT_EQ:
5259+
return op1 == op2;
5260+
5261+
case GT_NE:
5262+
return op1 != op2;
5263+
5264+
case GT_LT:
5265+
return op1 < op2;
5266+
5267+
case GT_LE:
5268+
return op1 <= op2;
5269+
5270+
case GT_GT:
5271+
return op1 > op2;
5272+
5273+
case GT_GE:
5274+
return op1 >= op2;
5275+
5276+
default:
5277+
unreached();
5278+
}
5279+
}
5280+
5281+
//------------------------------------------------------------------------
5282+
// CheckLoopConditionBaseCase: Verify that the loop condition is true when the
5283+
// loop is entered (or validated immediately on entry).
5284+
//
5285+
// Returns:
5286+
// True if we could prove that the condition is true at all interesting
5287+
// points in the loop.
5288+
//
5289+
// Remarks:
5290+
// Currently handles the following cases:
5291+
// * The condition being trivially true in the first iteration (e.g. for (int i = 0; i < 3; i++))
5292+
// * The condition is checked before entry (often due to loop inversion)
5293+
//
5294+
bool FlowGraphNaturalLoop::CheckLoopConditionBaseCase(BasicBlock* initBlock, NaturalLoopIterInfo* info)
5295+
{
5296+
// TODO: A common loop idiom is to enter the loop at the test, with the
5297+
// unique in-loop predecessor of the header block being the increment. We
5298+
// currently do not handle these patterns in `optExtractInitTestIncr`.
5299+
// Instead we depend on loop inversion to put them into an
5300+
// if (x) { do { ... } while (x) }
5301+
// form. Once we handle the pattern in `optExtractInitTestIncr` we can
5302+
// handle it here by checking for whether the test is the header and first
5303+
// thing in the header.
5304+
5305+
// Is it trivially true?
5306+
if (info->HasConstInit && info->HasConstLimit)
5307+
{
5308+
int initVal = info->ConstInitValue;
5309+
int limitVal = info->ConstLimit();
5310+
5311+
assert(genActualType(info->TestTree->gtGetOp1()) == TYP_INT);
5312+
5313+
bool isTriviallyTrue = info->TestTree->IsUnsigned()
5314+
? EvaluateRelop<uint32_t>((uint32_t)initVal, (uint32_t)limitVal, info->TestOper())
5315+
: EvaluateRelop<int32_t>(initVal, limitVal, info->TestOper());
5316+
5317+
if (isTriviallyTrue)
5318+
{
5319+
JITDUMP(" Condition is trivially true on entry (%d %s%s %d)\n", initVal,
5320+
info->TestTree->IsUnsigned() ? "(uns)" : "", GenTree::OpName(info->TestOper()), limitVal);
5321+
return true;
5322+
}
5323+
}
5324+
5325+
// Do we have a zero-trip test?
5326+
if (initBlock->KindIs(BBJ_COND))
5327+
{
5328+
GenTree* enteringJTrue = initBlock->lastStmt()->GetRootNode();
5329+
assert(enteringJTrue->OperIs(GT_JTRUE));
5330+
if (enteringJTrue->gtGetOp1()->OperIsCompare() && ((enteringJTrue->gtGetOp1()->gtFlags & GTF_RELOP_ZTT) != 0))
5331+
{
5332+
JITDUMP(" Condition is established before entry at [%06u]\n",
5333+
Compiler::dspTreeID(enteringJTrue->gtGetOp1()));
5334+
return true;
5335+
}
5336+
}
5337+
5338+
return false;
5339+
}
5340+
52155341
//------------------------------------------------------------------------
52165342
// GetLexicallyTopMostBlock: Get the lexically top-most block contained within
52175343
// the loop.
@@ -5340,7 +5466,7 @@ var_types NaturalLoopIterInfo::IterOperType()
53405466
//
53415467
bool NaturalLoopIterInfo::IsReversed()
53425468
{
5343-
return TestTree->gtGetOp2()->OperIs(GT_LCL_VAR) && ((TestTree->gtGetOp2()->gtFlags & GTF_VAR_ITERATOR) != 0);
5469+
return TestTree->gtGetOp2()->OperIsScalarLocal() && (TestTree->gtGetOp2()->AsLclVar()->GetLclNum() == IterVar);
53445470
}
53455471

53465472
//------------------------------------------------------------------------
@@ -5353,7 +5479,15 @@ bool NaturalLoopIterInfo::IsReversed()
53535479
genTreeOps NaturalLoopIterInfo::TestOper()
53545480
{
53555481
genTreeOps op = TestTree->OperGet();
5356-
return IsReversed() ? GenTree::SwapRelop(op) : op;
5482+
if (IsReversed())
5483+
{
5484+
op = GenTree::SwapRelop(op);
5485+
}
5486+
if (ExitedOnTrue)
5487+
{
5488+
op = GenTree::ReverseRelop(op);
5489+
}
5490+
return op;
53575491
}
53585492

53595493
//------------------------------------------------------------------------

src/coreclr/jit/loopcloning.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,9 @@ bool Compiler::optIsLoopClonable(FlowGraphNaturalLoop* loop, LoopCloneContext* c
18191819

18201820
// Is the entry block a handler or filter start? If so, then if we cloned, we could create a jump
18211821
// into the middle of a handler (to go to the cloned copy.) Reject.
1822+
// TODO: This seems like it can be deleted. If the header is the beginning
1823+
// of a handler then the loop should be fully contained within the handler,
1824+
// and the cloned loop will also be in the handler.
18221825
if (bbIsHandlerBeg(loop->GetHeader()))
18231826
{
18241827
JITDUMP("Loop cloning: rejecting loop " FMT_LP ". Header block is a handler start.\n", loop->GetIndex());
@@ -1873,6 +1876,8 @@ bool Compiler::optIsLoopClonable(FlowGraphNaturalLoop* loop, LoopCloneContext* c
18731876
return false;
18741877
}
18751878

1879+
// TODO-Quirk: Can be removed, loop invariant is validated by
1880+
// `FlowGraphNaturalLoop::AnalyzeIteration`.
18761881
if (!iterInfo->TestTree->OperIsCompare() || ((iterInfo->TestTree->gtFlags & GTF_RELOP_ZTT) == 0))
18771882
{
18781883
JITDUMP("Loop cloning: rejecting loop " FMT_LP

0 commit comments

Comments
 (0)