diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 2d4a84a3e41da4..f2989ae7b45a72 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -904,6 +904,10 @@ void Compiler::optPrintAssertion(const AssertionDsc& curAssertion, AssertionInde curAssertion.GetOp2().GetCheckedBoundConstant()); break; + case O2K_VN: + printf("VN " FMT_VN "", curAssertion.GetOp2().GetVN()); + break; + default: unreached(); break; @@ -1421,6 +1425,13 @@ AssertionIndex Compiler::optAddAssertion(const AssertionDsc& newAssertion) mayHaveDuplicates |= optAssertionHasAssertionsForVN(addOpVN, /* addIfNotFound */ canAddNewAssertions); } } + else if (newAssertion.GetOp2().KindIs(O2K_VN)) + { + // For VN VN assertions, register op2's VN too so consumers can find + // the assertion when iterating from the op2 side. + mayHaveDuplicates |= optAssertionHasAssertionsForVN(newAssertion.GetOp2().GetVN(), + /* addIfNotFound */ canAddNewAssertions); + } if (mayHaveDuplicates) { @@ -1550,6 +1561,12 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const assert(optLocalAssertionProp); break; + case O2K_VN: + assert(!optLocalAssertionProp); + assert(assertion.GetOp1().KindIs(O1K_VN)); + assert(assertion.IsRelop()); + break; + case O2K_ZEROOBJ: // We only make these assertion for stores (not control flow). assert(assertion.KindIs(OAK_EQUAL)); @@ -1807,6 +1824,24 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) return idx; } + // "X relop Y" where neither side is a constant nor a checked bound. + // For now, we only create such assertions for signed comparisons of int32 (and smaller, after promotion). + // This widens what global assertion prop can reason about: e.g. "b > a" combined with "a > 10" + // can be used to deduce "b > 10". + // + // To keep table pressure under control, we only create the assertion if at least one of the + // operands already has assertions registered. Otherwise the new assertion has no other facts + // it can chain with and is unlikely to enable any deduction, while still consuming a slot + // (and potentially crowding out useful ones). + if (!isUnsignedRelop && (op1VN != op2VN) && !vnStore->IsVNConstant(op1VN) && !vnStore->IsVNConstant(op2VN) && + (optAssertionHasAssertionsForVN(op1VN) || optAssertionHasAssertionsForVN(op2VN))) + { + AssertionDsc dsc = AssertionDsc::CreateRelopVN(this, relopFunc, op1VN, op2VN); + AssertionIndex idx = optAddAssertion(dsc); + optCreateComplementaryAssertion(idx); + return idx; + } + return NO_ASSERTION_INDEX; } diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index f964828161fac9..81af9929c5a008 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -7984,7 +7984,8 @@ class Compiler // nor it implies that it's never negative. O2K_ZEROOBJ, O2K_SUBRANGE, - O2K_CONST_VEC + O2K_CONST_VEC, + O2K_VN, // op2 is an arbitrary value number (used for VN VN assertions in global prop). }; struct AssertionDsc @@ -8130,7 +8131,7 @@ class Compiler ValueNum GetVN() const { assert(!m_compiler->optLocalAssertionProp); - assert(KindIs(O2K_CONST_INT, O2K_CONST_DOUBLE, O2K_ZEROOBJ, O2K_CONST_VEC)); + assert(KindIs(O2K_CONST_INT, O2K_CONST_DOUBLE, O2K_ZEROOBJ, O2K_CONST_VEC, O2K_VN)); assert(m_vn != ValueNumStore::NoVN); return m_vn; } @@ -8481,6 +8482,9 @@ class Compiler case O2K_SUBRANGE: return GetOp2().GetIntegralRange().Equals(that.GetOp2().GetIntegralRange()); + case O2K_VN: + return GetOp2().GetVN() == that.GetOp2().GetVN(); + default: assert(!"Unexpected value for GetOp2().m_kind in AssertionDsc."); break; @@ -8740,6 +8744,24 @@ class Compiler dsc.m_op2.m_icon.m_iconVal = cns; return dsc; } + + // Create "op1VN op2VN" assertion where both operands are arbitrary value numbers + // (used by global assertion prop for VN-to-VN signed comparisons of int32 and smaller). + static AssertionDsc CreateRelopVN(const Compiler* comp, VNFunc relop, ValueNum op1VN, ValueNum op2VN) + { + assert(!comp->optLocalAssertionProp); + assert(op1VN != ValueNumStore::NoVN); + assert(op2VN != ValueNumStore::NoVN); + assert(op1VN != op2VN); + + AssertionDsc dsc = CreateEmptyAssertion(comp); + dsc.m_assertionKind = FromVNFunc(relop); + dsc.m_op1.m_kind = O1K_VN; + dsc.m_op1.m_vn = op1VN; + dsc.m_op2.m_kind = O2K_VN; + dsc.m_op2.m_vn = op2VN; + return dsc; + } }; protected: diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 55a2c5ab7e3545..2d92e6cc0a8ab9 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -938,7 +938,12 @@ Range RangeCheck::GetRangeFromAssertionsWorker( result = phiRange; } - MergeEdgeAssertions(comp, num, ValueNumStore::NoVN, assertions, &result, false); + // MergeEdgeAssertionsWorker may recursively call back to GetRangeFromAssertionsWorker for other VN-to-VN assertion + // lookups. + int edgeAssertionsBudget = min(3, budget); + MergeEdgeAssertionsWorker(comp, num, ValueNumStore::NoVN, assertions, &result, /* canUseCheckedBounds */ false, + edgeAssertionsBudget, visited); + assert(result.IsConstantRange()); return result; } @@ -1044,6 +1049,37 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, ASSERT_VALARG_TP assertions, Range* pRange, bool canUseCheckedBounds) +{ + // Public entry point: no shared visited set; create a fresh one and use a small recursion + // budget for VN-to-VN assertion lookups. + ValueNumStore::SmallValueNumSet visited; + MergeEdgeAssertionsWorker(comp, normalLclVN, preferredBoundVN, assertions, pRange, canUseCheckedBounds, + /* budget */ 3, &visited); +} + +//------------------------------------------------------------------------ +// MergeEdgeAssertionsWorker: Worker for MergeEdgeAssertions that takes a visited set and recursion +// budget for VN-to-VN assertion lookups. +// +// Arguments: +// comp - the compiler instance +// normalLclVN - the value number to look for assertions for +// preferredBoundVN - when this VN is set, it will be given preference over constant limits +// assertions - the assertions to use +// pRange - the range to tighten with assertions +// canUseCheckedBounds - true if we can use checked bounds assertions (cache) +// budget - the remaining budget for recursive VN-to-VN assertion lookups +// visited - the set of value numbers already visited in the current search path to prevent infinite +// recursion +// +void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp, + ValueNum normalLclVN, + ValueNum preferredBoundVN, + ASSERT_VALARG_TP assertions, + Range* pRange, + bool canUseCheckedBounds, + int budget, + ValueNumStore::SmallValueNumSet* visited) { Range assertedRange = Range(Limit(Limit::keUnknown)); if (BitVecOps::IsEmpty(comp->apTraits, assertions)) @@ -1343,6 +1379,76 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, continue; } } + // Current assertion is of the form "X Y" where both X and Y are arbitrary VNs + // We try to derive a bound on normalLclVN by recursively computing the range of the other operand. + // + // Example: + // + // if (a > 10 && a < 100) + // { + // if (normalLclVN > a) // a is an unknown VN with [11..99] range derived from assertions. + // { + // + else if (curAssertion.IsRelop() && curAssertion.GetOp2().KindIs(Compiler::O2K_VN) && + (curAssertion.GetOp1().GetVN() == normalLclVN || curAssertion.GetOp2().GetVN() == normalLclVN)) + { + ValueNum op1VN = curAssertion.GetOp1().GetVN(); + ValueNum op2VN = curAssertion.GetOp2().GetVN(); + + cmpOper = Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned); + if (isUnsigned) + { + continue; + } + + ValueNum otherVN; + if (op1VN == normalLclVN) + { + // Assertion is "normalLclVN otherVN" - keep cmpOper as-is. + otherVN = op2VN; + } + else + { + // Assertion is "otherVN normalLclVN" - swap to get + // "normalLclVN otherVN". + assert(op2VN == normalLclVN); + otherVN = op1VN; + cmpOper = GenTree::SwapRelop(cmpOper); + } + + if (budget <= 0) + { + continue; + } + + budget--; + Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget, visited); + if (!otherRange.IsConstantRange()) + { + continue; + } + + // Derive a constant limit for normalLclVN from the constant range of otherVN. + // We use the most useful bound for each direction of the comparison. + int derivedLimit; + switch (cmpOper) + { + case GT_LT: + case GT_LE: + // normalLclVN < otherVN (or <=) => normalLclVN <(=) otherVN.UpperLimit + derivedLimit = otherRange.UpperLimit().GetConstant(); + break; + case GT_GT: + case GT_GE: + // normalLclVN > otherVN (or >=) => normalLclVN >(=) otherVN.LowerLimit + derivedLimit = otherRange.LowerLimit().GetConstant(); + break; + + default: + continue; + } + limit = Limit(Limit::keConstant, derivedLimit); + } // Current assertion is not supported, ignore it else { diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index cab98b29c85b60..ea399f334edbeb 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -827,6 +827,18 @@ class RangeCheck Range* pRange, bool canUseCheckedBounds = true); + // Internal worker used by GetRangeFromAssertionsWorker: same as the public overload + // but threads a recursion budget and visited set so that VN-to-VN assertions can be + // resolved by recursing into GetRangeFromAssertionsWorker without unbounded work. + static void MergeEdgeAssertionsWorker(Compiler* comp, + ValueNum num, + ValueNum preferredBoundVN, + ASSERT_VALARG_TP assertions, + Range* pRange, + bool canUseCheckedBounds, + int budget, + ValueNumStore::SmallValueNumSet* visited); + // The maximum possible value of the given "limit". If such a value could not be determined // return "false". For example: CORINFO_Array_MaxLength for array length. bool GetLimitMax(Limit& limit, int* pMax);