Skip to content

Commit 30e643e

Browse files
authored
Jit: Remove bounds checks with tests against length. (#40180)
* Introduce a concept of minimum array length into range check * Some cleanup * fix potential underflow * bug fix and cleanup * Revert string changes * Allow elimination of arr[0] with len != 0 test * Revert "Revert string changes" This reverts commit 6f77bf8. * Fix up tests * reverting lower bound merging as it may be unsound * Fix CI exposed bug and add a couple of test cases * code review feedback * comment nit * feedback * Add missing break
1 parent d1c0fa8 commit 30e643e

7 files changed

Lines changed: 699 additions & 56 deletions

File tree

src/coreclr/src/jit/assertionprop.cpp

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,6 @@ void Compiler::optDebugCheckAssertion(AssertionDsc* assertion)
16251625
assert(assertion->op2.u1.iconFlags != 0);
16261626
break;
16271627
case O1K_LCLVAR:
1628-
case O1K_ARR_BND:
16291628
assert((lvaTable[assertion->op1.lcl.lclNum].lvType != TYP_REF) || (assertion->op2.u1.iconVal == 0));
16301629
break;
16311630
case O1K_VALUE_NUMBER:
@@ -1959,12 +1958,58 @@ AssertionInfo Compiler::optAssertionGenJtrue(GenTree* tree)
19591958
{
19601959
std::swap(op1, op2);
19611960
}
1961+
1962+
ValueNum op1VN = vnStore->VNConservativeNormalValue(op1->gtVNPair);
1963+
ValueNum op2VN = vnStore->VNConservativeNormalValue(op2->gtVNPair);
19621964
// If op1 is lcl and op2 is const or lcl, create assertion.
19631965
if ((op1->gtOper == GT_LCL_VAR) &&
19641966
((op2->OperKind() & GTK_CONST) || (op2->gtOper == GT_LCL_VAR))) // Fix for Dev10 851483
19651967
{
19661968
return optCreateJtrueAssertions(op1, op2, assertionKind);
19671969
}
1970+
else if (vnStore->IsVNCheckedBound(op1VN) && vnStore->IsVNInt32Constant(op2VN))
1971+
{
1972+
assert(relop->OperIs(GT_EQ, GT_NE));
1973+
1974+
int con = vnStore->ConstantValue<int>(op2VN);
1975+
if (con >= 0)
1976+
{
1977+
AssertionDsc dsc;
1978+
1979+
// For arr.Length != 0, we know that 0 is a valid index
1980+
// For arr.Length == con, we know that con - 1 is the greatest valid index
1981+
if (con == 0)
1982+
{
1983+
dsc.assertionKind = OAK_NOT_EQUAL;
1984+
dsc.op1.bnd.vnIdx = vnStore->VNForIntCon(0);
1985+
}
1986+
else
1987+
{
1988+
dsc.assertionKind = OAK_EQUAL;
1989+
dsc.op1.bnd.vnIdx = vnStore->VNForIntCon(con - 1);
1990+
}
1991+
1992+
dsc.op1.vn = op1VN;
1993+
dsc.op1.kind = O1K_ARR_BND;
1994+
dsc.op1.bnd.vnLen = op1VN;
1995+
dsc.op2.vn = vnStore->VNConservativeNormalValue(op2->gtVNPair);
1996+
dsc.op2.kind = O2K_CONST_INT;
1997+
dsc.op2.u1.iconFlags = 0;
1998+
dsc.op2.u1.iconVal = 0;
1999+
2000+
// when con is not zero, create an assertion on the arr.Length == con edge
2001+
// when con is zero, create an assertion on the arr.Length != 0 edge
2002+
AssertionIndex index = optAddAssertion(&dsc);
2003+
if (relop->OperIs(GT_NE) != (con == 0))
2004+
{
2005+
return AssertionInfo::ForNextEdge(index);
2006+
}
2007+
else
2008+
{
2009+
return index;
2010+
}
2011+
}
2012+
}
19682013

19692014
// Check op1 and op2 for an indirection of a GT_LCL_VAR and keep it in op1.
19702015
if (((op1->gtOper != GT_IND) || (op1->AsOp()->gtOp1->gtOper != GT_LCL_VAR)) &&

src/coreclr/src/jit/compiler.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6912,6 +6912,11 @@ class Compiler
69126912
return ((assertionKind == OAK_EQUAL) && (op1.kind == O1K_LCLVAR) && (op2.kind == O2K_LCLVAR_COPY));
69136913
}
69146914

6915+
bool IsConstantInt32Assertion()
6916+
{
6917+
return ((assertionKind == OAK_EQUAL) || (assertionKind == OAK_NOT_EQUAL)) && (op2.kind == O2K_CONST_INT);
6918+
}
6919+
69156920
static bool SameKind(AssertionDsc* a1, AssertionDsc* a2)
69166921
{
69176922
return a1->assertionKind == a2->assertionKind && a1->op1.kind == a2->op1.kind &&

src/coreclr/src/jit/rangecheck.cpp

Lines changed: 140 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,29 @@ int RangeCheck::GetArrLength(ValueNum vn)
5959
return m_pCompiler->vnStore->GetNewArrSize(arrRefVN);
6060
}
6161

62-
// Check if the computed range is within bounds.
63-
bool RangeCheck::BetweenBounds(Range& range, int lower, GenTree* upper)
62+
//------------------------------------------------------------------------
63+
// BetweenBounds: Check if the computed range is within bounds
64+
//
65+
// Arguments:
66+
// Range - the range to check if in bounds
67+
// upper - the array length vn
68+
// arrSize - the length of the array if known, or <= 0
69+
//
70+
// Return Value:
71+
// True iff range is between [0 and vn - 1] or [0, arrSize - 1]
72+
//
73+
// notes:
74+
// This function assumes that the lower range is resolved and upper range is symbolic as in an
75+
// increasing loop.
76+
//
77+
// TODO-CQ: This is not general enough.
78+
//
79+
bool RangeCheck::BetweenBounds(Range& range, GenTree* upper, int arrSize)
6480
{
6581
#ifdef DEBUG
6682
if (m_pCompiler->verbose)
6783
{
68-
printf("%s BetweenBounds <%d, ", range.ToString(m_pCompiler->getAllocatorDebugOnly()), lower);
84+
printf("%s BetweenBounds <%d, ", range.ToString(m_pCompiler->getAllocatorDebugOnly()), 0);
6985
Compiler::printTreeID(upper);
7086
printf(">\n");
7187
}
@@ -85,28 +101,9 @@ bool RangeCheck::BetweenBounds(Range& range, int lower, GenTree* upper)
85101
JITDUMP("\n");
86102
#endif
87103

88-
int arrSize = 0;
89-
90-
if (vnStore->IsVNConstant(uLimitVN))
104+
if ((arrSize <= 0) && !vnStore->IsVNCheckedBound(uLimitVN))
91105
{
92-
ssize_t constVal = -1;
93-
unsigned iconFlags = 0;
94-
95-
if (m_pCompiler->optIsTreeKnownIntValue(true, upper, &constVal, &iconFlags))
96-
{
97-
arrSize = (int)constVal;
98-
}
99-
}
100-
else if (vnStore->IsVNArrLen(uLimitVN))
101-
{
102-
// Get the array reference from the length.
103-
ValueNum arrRefVN = vnStore->GetArrForLenVn(uLimitVN);
104-
// Check if array size can be obtained.
105-
arrSize = vnStore->GetNewArrSize(arrRefVN);
106-
}
107-
else if (!vnStore->IsVNCheckedBound(uLimitVN))
108-
{
109-
// If the upper limit is not length, then bail.
106+
// If we don't know the array size and the upper limit is not known, then bail.
110107
return false;
111108
}
112109

@@ -231,6 +228,19 @@ void RangeCheck::OptimizeRangeCheck(BasicBlock* block, Statement* stmt, GenTree*
231228
#endif // FEATURE_SIMD
232229
{
233230
arrSize = GetArrLength(arrLenVn);
231+
232+
// if we can't find the array length, see if there
233+
// are any assertions about the array size we can use to get a minimum length
234+
if (arrSize <= 0)
235+
{
236+
JITDUMP("Looking for array size assertions for: " FMT_VN "\n", arrLenVn);
237+
Range arrLength = Range(Limit(Limit::keDependent));
238+
MergeEdgeAssertions(arrLenVn, block->bbAssertionIn, &arrLength);
239+
if (arrLength.lLimit.IsConstant())
240+
{
241+
arrSize = arrLength.lLimit.GetConstant();
242+
}
243+
}
234244
}
235245

236246
JITDUMP("ArrSize for lengthVN:%03X = %d\n", arrLenVn, arrSize);
@@ -286,7 +296,7 @@ void RangeCheck::OptimizeRangeCheck(BasicBlock* block, Statement* stmt, GenTree*
286296
}
287297

288298
// Is the range between the lower and upper bound values.
289-
if (BetweenBounds(range, 0, bndsChk->gtArrLen))
299+
if (BetweenBounds(range, bndsChk->gtArrLen, arrSize))
290300
{
291301
JITDUMP("[RangeCheck::OptimizeRangeCheck] Between bounds\n");
292302
m_pCompiler->optRemoveRangeCheck(treeParent, stmt);
@@ -532,18 +542,51 @@ void RangeCheck::SetDef(UINT64 hash, Location* loc)
532542
}
533543
#endif
534544

535-
// Merge assertions on the edge flowing into the block about a variable.
545+
//------------------------------------------------------------------------
546+
// MergeEdgeAssertions: Merge assertions on the edge flowing into the block about a variable
547+
//
548+
// Arguments:
549+
// GenTreeLclVarCommon - the variable to look for assertions for
550+
// assertions - the assertions to use
551+
// pRange - the range to tighten with assertions
552+
//
536553
void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP assertions, Range* pRange)
554+
{
555+
if (lcl->GetSsaNum() == SsaConfig::RESERVED_SSA_NUM)
556+
{
557+
return;
558+
}
559+
560+
LclVarDsc* varDsc = m_pCompiler->lvaGetDesc(lcl);
561+
if (varDsc->CanBeReplacedWithItsField(m_pCompiler))
562+
{
563+
varDsc = m_pCompiler->lvaGetDesc(varDsc->lvFieldLclStart);
564+
}
565+
LclSsaVarDsc* ssaData = varDsc->GetPerSsaData(lcl->GetSsaNum());
566+
ValueNum normalLclVN = m_pCompiler->vnStore->VNConservativeNormalValue(ssaData->m_vnPair);
567+
MergeEdgeAssertions(normalLclVN, assertions, pRange);
568+
}
569+
570+
//------------------------------------------------------------------------
571+
// MergeEdgeAssertions: Merge assertions on the edge flowing into the block about a variable
572+
//
573+
// Arguments:
574+
// normalLclVN - the value number to look for assertions for
575+
// assertions - the assertions to use
576+
// pRange - the range to tighten with assertions
577+
//
578+
void RangeCheck::MergeEdgeAssertions(ValueNum normalLclVN, ASSERT_VALARG_TP assertions, Range* pRange)
537579
{
538580
if (BitVecOps::IsEmpty(m_pCompiler->apTraits, assertions))
539581
{
540582
return;
541583
}
542584

543-
if (lcl->GetSsaNum() == SsaConfig::RESERVED_SSA_NUM)
585+
if (normalLclVN == ValueNumStore::NoVN)
544586
{
545587
return;
546588
}
589+
547590
// Walk through the "assertions" to check if the apply.
548591
BitVecOps::Iter iter(m_pCompiler->apTraits, assertions);
549592
unsigned index = 0;
@@ -554,15 +597,8 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
554597
Compiler::AssertionDsc* curAssertion = m_pCompiler->optGetAssertion(assertionIndex);
555598

556599
Limit limit(Limit::keUndef);
557-
genTreeOps cmpOper = GT_NONE;
558-
559-
LclVarDsc* varDsc = m_pCompiler->lvaGetDesc(lcl);
560-
if (varDsc->CanBeReplacedWithItsField(m_pCompiler))
561-
{
562-
varDsc = m_pCompiler->lvaGetDesc(varDsc->lvFieldLclStart);
563-
}
564-
LclSsaVarDsc* ssaData = varDsc->GetPerSsaData(lcl->GetSsaNum());
565-
ValueNum normalLclVN = m_pCompiler->vnStore->VNConservativeNormalValue(ssaData->m_vnPair);
600+
genTreeOps cmpOper = GT_NONE;
601+
bool isConstantAssertion = false;
566602

567603
// Current assertion is of the form (i < len - cns) != 0
568604
if (curAssertion->IsCheckedBoundArithBound())
@@ -602,13 +638,20 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
602638
m_pCompiler->vnStore->GetCompareCheckedBound(curAssertion->op1.vn, &info);
603639

604640
// If we don't have the same variable we are comparing against, bail.
605-
if (normalLclVN != info.cmpOp)
641+
if (normalLclVN == info.cmpOp)
642+
{
643+
cmpOper = (genTreeOps)info.cmpOper;
644+
limit = Limit(Limit::keBinOpArray, info.vnBound, 0);
645+
}
646+
else if (normalLclVN == info.vnBound)
647+
{
648+
cmpOper = GenTree::SwapRelop((genTreeOps)info.cmpOper);
649+
limit = Limit(Limit::keBinOpArray, info.cmpOp, 0);
650+
}
651+
else
606652
{
607653
continue;
608654
}
609-
610-
limit = Limit(Limit::keBinOpArray, info.vnBound, 0);
611-
cmpOper = (genTreeOps)info.cmpOper;
612655
}
613656
// Current assertion is of the form (i < 100) != 0
614657
else if (curAssertion->IsConstantBound())
@@ -627,6 +670,36 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
627670
limit = Limit(Limit::keConstant, info.constVal);
628671
cmpOper = (genTreeOps)info.cmpOper;
629672
}
673+
// Current assertion is of the form i == 100
674+
else if (curAssertion->IsConstantInt32Assertion())
675+
{
676+
if (curAssertion->op1.vn != normalLclVN)
677+
{
678+
continue;
679+
}
680+
681+
int cnstLimit = m_pCompiler->vnStore->CoercedConstantValue<int>(curAssertion->op2.vn);
682+
683+
if ((cnstLimit == 0) && (curAssertion->assertionKind == Compiler::OAK_NOT_EQUAL) &&
684+
m_pCompiler->vnStore->IsVNCheckedBound(curAssertion->op1.vn))
685+
{
686+
// we have arr.Len != 0, so the length must be atleast one
687+
limit = Limit(Limit::keConstant, 1);
688+
cmpOper = GT_GE;
689+
}
690+
else if (curAssertion->assertionKind == Compiler::OAK_EQUAL)
691+
{
692+
limit = Limit(Limit::keConstant, cnstLimit);
693+
cmpOper = GT_EQ;
694+
}
695+
else
696+
{
697+
// We have a != assertion, but it doesn't tell us much about the interval. So just skip it.
698+
continue;
699+
}
700+
701+
isConstantAssertion = true;
702+
}
630703
// Current assertion is not supported, ignore it
631704
else
632705
{
@@ -635,8 +708,8 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
635708

636709
assert(limit.IsBinOpArray() || limit.IsConstant());
637710

638-
// Make sure the assertion is of the form != 0 or == 0.
639-
if (curAssertion->op2.vn != m_pCompiler->vnStore->VNZeroForType(TYP_INT))
711+
// Make sure the assertion is of the form != 0 or == 0 if it isn't a constant assertion.
712+
if (!isConstantAssertion && (curAssertion->op2.vn != m_pCompiler->vnStore->VNZeroForType(TYP_INT)))
640713
{
641714
continue;
642715
}
@@ -647,6 +720,17 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
647720
}
648721
#endif
649722

723+
// Limits are sometimes made with the form vn + constant, where vn is a known constant
724+
// see if we can simplify this to just a constant
725+
if (limit.IsBinOpArray() && m_pCompiler->vnStore->IsVNInt32Constant(limit.vn))
726+
{
727+
Limit tempLimit = Limit(Limit::keConstant, m_pCompiler->vnStore->ConstantValue<int>(limit.vn));
728+
if (tempLimit.AddConstant(limit.cns))
729+
{
730+
limit = tempLimit;
731+
}
732+
}
733+
650734
ValueNum arrLenVN = m_pCompiler->vnStore->VNConservativeNormalValue(m_pCurBndsChk->gtArrLen->gtVNPair);
651735

652736
if (m_pCompiler->vnStore->IsVNConstant(arrLenVN))
@@ -663,22 +747,25 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
663747
// (i < length) != 0
664748
// (i < 100) == 0
665749
// (i < 100) != 0
750+
// i == 100
666751
//
667-
// At this point, we have detected that op1.vn is (i < length) or (i < length + cns) or
668-
// (i < 100) and the op2.vn is 0.
752+
// At this point, we have detected that either op1.vn is (i < length) or (i < length + cns) or
753+
// (i < 100) and the op2.vn is 0 or that op1.vn is i and op2.vn is a known constant.
669754
//
670755
// Now, let us check if we are == 0 (i.e., op1 assertion is false) or != 0 (op1 assertion
671-
// is true.),
756+
// is true.).
672757
//
673-
// If we have an assertion of the form == 0 (i.e., equals false), then reverse relop.
758+
// If we have a non-constant assertion of the form == 0 (i.e., equals false), then reverse relop.
674759
// The relop has to be reversed because we have: (i < length) is false which is the same
675760
// as (i >= length).
676-
if (curAssertion->assertionKind == Compiler::OAK_EQUAL)
761+
if ((curAssertion->assertionKind == Compiler::OAK_EQUAL) && !isConstantAssertion)
677762
{
678763
cmpOper = GenTree::ReverseRelop(cmpOper);
679764
}
680765

681-
// Bounds are inclusive, so add -1 for upper bound when "<". But make sure we won't overflow.
766+
assert(cmpOper != GT_NONE);
767+
768+
// Bounds are inclusive, so add -1 for upper bound when "<". But make sure we won't underflow.
682769
if (cmpOper == GT_LT && !limit.AddConstant(-1))
683770
{
684771
continue;
@@ -744,6 +831,11 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP
744831
pRange->lLimit = limit;
745832
break;
746833

834+
case GT_EQ:
835+
pRange->uLimit = limit;
836+
pRange->lLimit = limit;
837+
break;
838+
747839
default:
748840
// All other 'cmpOper' kinds leave lLimit/uLimit unchanged
749841
break;

0 commit comments

Comments
 (0)