Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/coreclr/jit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ set( JIT_SOURCES
jiteh.cpp
jithashtable.cpp
jitmetadata.cpp
knownbits.cpp
layout.cpp
lclmorph.cpp
lclvars.cpp
Expand Down Expand Up @@ -378,6 +379,7 @@ set( JIT_HEADERS
jitmetadatalist.h
jitpch.h
jitstd.h
knownbits.h
lir.h
loopcloning.h
loopcloningopts.h
Expand Down
222 changes: 218 additions & 4 deletions src/coreclr/jit/assertionprop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

#include "jitpch.h"
#include "rangecheck.h"
#include "knownbits.h"
#ifdef _MSC_VER
#pragma hdrstop
#endif
Expand Down Expand Up @@ -1786,9 +1787,17 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
ValueNum op1VN = relopFuncApp.GetArg(0);
ValueNum op2VN = relopFuncApp.GetArg(1);

if ((genActualType(vnStore->TypeOfVN(op1VN)) != TYP_INT) || (genActualType(vnStore->TypeOfVN(op2VN)) != TYP_INT))
{
// For now, we don't have consumers for assertions derived from non-int32 comparisons
const var_types op1Type = genActualType(vnStore->TypeOfVN(op1VN));
const var_types op2Type = genActualType(vnStore->TypeOfVN(op2VN));
const bool allow64 = (JitConfig.JitEnableKnownBits() != 0);
const bool op1TypeOk = (op1Type == TYP_INT) || (allow64 && (op1Type == TYP_LONG));
const bool op2TypeOk = (op2Type == TYP_INT) || (allow64 && (op2Type == TYP_LONG));
if (!op1TypeOk || !op2TypeOk)
{
// We only have consumers for assertions derived from int32 comparisons, plus int64 ones once
// KnownBits is enabled. Note: the checked-bound forms below are inherently int32 (array
// lengths), so they simply won't match for TYP_LONG; the "X relop CNS" form feeds Compute and
// the non-negativity checks for both widths.
return NO_ASSERTION_INDEX;
}

Expand Down Expand Up @@ -1882,7 +1891,8 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// 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) &&
if (!isUnsignedRelop && (op1Type == TYP_INT) && (op1VN != op2VN) && !vnStore->IsVNConstant(op1VN) &&
!vnStore->IsVNConstant(op2VN) &&
(optAssertionHasAssertionsForVN(op1VN) || optAssertionHasAssertionsForVN(op2VN)))
{
AssertionDsc dsc = AssertionDsc::CreateRelopVN(this, relopFunc, op1VN, op2VN);
Expand Down Expand Up @@ -4083,6 +4093,79 @@ void Compiler::optAssertionProp_RangeProperties(ASSERT_VALARG_TP assertions,
*isKnownNonZero = true;
}
}

// Known bits can also establish non-negativity (sign bit known 0) and non-zeroness (some bit
// known 1). This complements the interval range above: it additionally covers TYP_LONG (which
// has no range analysis) and bit patterns that an interval cannot express (e.g. "x & 7").
if (!*isKnownNonZero || !*isKnownNonNegative)
{
const unsigned width = (genActualType(tree) == TYP_LONG) ? 64 : 32;
const uint64_t signBit = 1ull << (width - 1);
const KnownBits kb = KnownBits::Compute(this, treeVN, assertions);
if ((kb.knownZero & signBit) != 0)
{
*isKnownNonNegative = true;
}
if (kb.knownOne != 0)
{
*isKnownNonZero = true;
}
}
}

//------------------------------------------------------------------------
// knownBitsOperCannotOverflow: Determine whether ADD/SUB/MUL on operands with the given known bits
// cannot overflow at "width" bits with the given signedness, based purely on the operands' ranges.
//
template <typename T>
static bool knownBitsOperCannotOverflowT(
genTreeOps oper, bool isUnsigned, const KnownBits& a, const KnownBits& b, unsigned width)
{
if (isUnsigned)
{
const T aMax = (T)a.GetUMax(width);
const T bMax = (T)b.GetUMax(width);
switch (oper)
{
case GT_ADD:
return !CheckedOps::AddOverflows<T>(aMax, bMax, CheckedOps::Unsigned);
case GT_SUB:
// Unsigned a - b underflows iff a < b; safe iff umin(a) >= umax(b).
return a.GetUMin(width) >= b.GetUMax(width);
case GT_MUL:
return !CheckedOps::MulOverflows<T>(aMax, bMax, CheckedOps::Unsigned);
default:
return false;
}
}
Comment on lines +4124 to +4140

const T aMin = (T)a.GetSMin(width);
const T aMax = (T)a.GetSMax(width);
const T bMin = (T)b.GetSMin(width);
const T bMax = (T)b.GetSMax(width);
switch (oper)
{
case GT_ADD:
return !CheckedOps::AddOverflows<T>(aMin, bMin, CheckedOps::Signed) &&
!CheckedOps::AddOverflows<T>(aMax, bMax, CheckedOps::Signed);
case GT_SUB:
return !CheckedOps::SubOverflows<T>(aMin, bMax, CheckedOps::Signed) &&
!CheckedOps::SubOverflows<T>(aMax, bMin, CheckedOps::Signed);
case GT_MUL:
return !CheckedOps::MulOverflows<T>(aMin, bMin, CheckedOps::Signed) &&
!CheckedOps::MulOverflows<T>(aMin, bMax, CheckedOps::Signed) &&
!CheckedOps::MulOverflows<T>(aMax, bMin, CheckedOps::Signed) &&
!CheckedOps::MulOverflows<T>(aMax, bMax, CheckedOps::Signed);
default:
return false;
}
}

static bool knownBitsOperCannotOverflow(
genTreeOps oper, bool isUnsigned, const KnownBits& a, const KnownBits& b, unsigned width)
{
return (width == 32) ? knownBitsOperCannotOverflowT<int32_t>(oper, isUnsigned, a, b, width)
: knownBitsOperCannotOverflowT<int64_t>(oper, isUnsigned, a, b, width);
}

//------------------------------------------------------------------------
Expand Down Expand Up @@ -4131,6 +4214,20 @@ GenTree* Compiler::optAssertionProp_AddMulSub(ASSERT_VALARG_TP assertions, GenTr
return optAssertionProp_Update(tree, tree, stmt);
}
}

// Known-bits based no-overflow proof. This also covers TYP_LONG operations (which the range-based
// path above does not handle) and bit patterns an interval range cannot express.
if (!optLocalAssertionProp && tree->gtOverflow() && varTypeIsIntegral(tree))
{
const unsigned width = (genActualType(tree) == TYP_LONG) ? 64 : 32;
const KnownBits kb1 = KnownBits::Compute(this, optConservativeNormalVN(tree->gtGetOp1()), assertions);
const KnownBits kb2 = KnownBits::Compute(this, optConservativeNormalVN(tree->gtGetOp2()), assertions);
if (knownBitsOperCannotOverflow(tree->OperGet(), tree->IsUnsigned(), kb1, kb2, width))
{
tree->ClearOverflow();
return optAssertionProp_Update(tree, tree, stmt);
Comment on lines +4218 to +4228
}
}
return nullptr;
}

Expand Down Expand Up @@ -4514,6 +4611,24 @@ GenTree* Compiler::optAssertionPropGlobal_RelOp(ASSERT_VALARG_TP assertions,
}
}

// See if we can fold the relop based on known bits. This complements the range-based folding
// above (which is limited to TYP_INT) by reasoning about individual bits and TYP_LONG values,
// and covers signed and unsigned ordering as well as equality.
if (varTypeIsIntegral(op1) && !varTypeIsGC(op1) && (op1VN != ValueNumStore::NoVN) && (op2VN != ValueNumStore::NoVN))
{
const unsigned width = (genActualType(op1) == TYP_LONG) ? 64 : 32;
const KnownBits kb1 = KnownBits::Compute(this, op1VN, assertions);
const KnownBits kb2 = KnownBits::Compute(this, op2VN, assertions);

const int relopResult = KnownBitsOps::EvalRelop(tree->OperGet(), tree->IsUnsigned(), kb1, kb2, width);
if (relopResult >= 0)
{
JITDUMP("Folding relop [%06u] based on known bits.\n", dspTreeID(tree));
newTree = gtWrapWithSideEffects(relopResult == 1 ? gtNewTrue() : gtNewFalse(), tree, GTF_ALL_EFFECT);
return optAssertionProp_Update(newTree, tree, stmt);
}
}

// Else check if we have an equality check involving a local or an indir
if (!tree->OperIs(GT_EQ, GT_NE))
{
Expand Down Expand Up @@ -4954,6 +5069,32 @@ GenTree* Compiler::optAssertionProp_Cast(ASSERT_VALARG_TP assertions,
}
}
}

// Try the same proof using known bits. This also covers TYP_LONG cast operands (which the
// range-based path above does not handle) and bit patterns the range cannot express.
if (varTypeIsIntegral(cast->CastOp()) && !varTypeIsGC(cast->CastOp()))
{
const unsigned width = (genActualType(cast->CastOp()) == TYP_LONG) ? 64 : 32;
const ValueNum castOpVN = optConservativeNormalVN(cast->CastOp());
const KnownBits kb = KnownBits::Compute(this, castOpVN, assertions);

int64_t castFromLo;
int64_t castFromHi;
if (kb.TryGetSignedRange(width, &castFromLo, &castFromHi) && (castFromLo >= castLo) &&
(castFromHi <= castHi))
{
if (canDropCast)
{
JITDUMP("Removing cast %06u as redundant based on VN known-bits.\n", dspTreeID(cast));
return optAssertionProp_Update(cast->CastOp(), cast, stmt);
}

assert(cast->gtOverflow());
JITDUMP("Clearing overflow flag for cast %06u based on VN known-bits.\n", dspTreeID(cast));
cast->ClearOverflow();
return optAssertionProp_Update(cast, cast, stmt);
}
}
return nullptr;
}

Expand Down Expand Up @@ -5537,6 +5678,17 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree
return optAssertionProp_Update(newTree, arrBndsChk, stmt);
};

// Known-bits based elimination: if the index is provably (unsigned) less than the length, the
// check is redundant. Catches masked indices and bit patterns the range-based paths cannot express.
{
const KnownBits kbIdx = KnownBits::Compute(this, vnCurIdx, assertions);
const KnownBits kbLen = KnownBits::Compute(this, vnCurLen, assertions);
if (KnownBitsOps::EvalRelop(GT_LT, /* isUnsigned */ true, kbIdx, kbLen, 32) == 1)
{
return dropBoundsCheck(INDEBUG("known bits prove (uint)index < (uint)length"));
}
}

// First, check if we have arr[arr.Length - cns] when we know arr.Length is >= cns.
ValueNum add0, add1;
if (vnStore->IsVNBinFunc(vnCurIdx, VNF_ADD, &add0, &add1))
Expand Down Expand Up @@ -5779,6 +5931,64 @@ GenTree* Compiler::optAssertionProp_Update(GenTree* newTree, GenTree* tree, Stat
// Notes:
// stmt may be nullptr during local assertion prop
//
//------------------------------------------------------------------------
// optAssertionProp_KnownBitsSimplify: Remove a redundant constant mask on AND/OR using known bits:
// "x & C" == "x" when every possibly-set bit of x is set in C, and "x | C" == "x" when every set
// bit of C is already known-one in x.
//
GenTree* Compiler::optAssertionProp_KnownBitsSimplify(ASSERT_VALARG_TP assertions, GenTreeOp* tree, Statement* stmt)
{
if (optLocalAssertionProp || !varTypeIsIntegral(tree))
{
return nullptr;
}

GenTree* op1 = tree->gtGetOp1();
GenTree* op2 = tree->gtGetOp2();

// We need a constant mask on op2 with no side effects of its own (so dropping it is safe).
int64_t cns;
if ((op2->gtFlags & GTF_SIDE_EFFECT) != 0 ||
!vnStore->IsVNIntegralConstant<int64_t>(optConservativeNormalVN(op2), &cns))
{
return nullptr;
}

const unsigned width = (genActualType(tree) == TYP_LONG) ? 64 : 32;
const uint64_t mask = KnownBits::WidthMask(width);
const uint64_t c = (uint64_t)cns & mask;
const KnownBits kb = KnownBits::Compute(this, optConservativeNormalVN(op1), assertions);
const uint64_t maybeOne = ~kb.knownZero & mask;

if (tree->OperIs(GT_AND))
{
// x & C == x iff every possibly-set bit of x is set in C.
if ((maybeOne & ~c & mask) == 0)
{
JITDUMP("Removing redundant AND mask [%06u] based on known bits.\n", dspTreeID(tree));
return optAssertionProp_Update(op1, tree, stmt);
}
// x & C == 0 iff x has no possibly-set bit in C.
if ((maybeOne & c) == 0)
{
JITDUMP("Folding AND [%06u] to zero based on known bits.\n", dspTreeID(tree));
GenTree* zero = (width == 64) ? gtNewLconNode(0) : gtNewIconNode(0, genActualType(tree));
return optAssertionProp_Update(gtWrapWithSideEffects(zero, tree, GTF_ALL_EFFECT), tree, stmt);
}
}
else
{
assert(tree->OperIs(GT_OR));
// x | C == x iff every set bit of C is already known-one in x.
if ((c & ~kb.knownOne & mask) == 0)
{
JITDUMP("Removing redundant OR mask [%06u] based on known bits.\n", dspTreeID(tree));
return optAssertionProp_Update(op1, tree, stmt);
}
}
return nullptr;
}

GenTree* Compiler::optAssertionProp(ASSERT_VALARG_TP assertions, GenTree* tree, Statement* stmt, BasicBlock* block)
{
switch (tree->gtOper)
Expand Down Expand Up @@ -5848,6 +6058,10 @@ GenTree* Compiler::optAssertionProp(ASSERT_VALARG_TP assertions, GenTree* tree,
case GT_GE:
return optAssertionProp_RelOp(assertions, tree, stmt, block);

case GT_AND:
case GT_OR:
return optAssertionProp_KnownBitsSimplify(assertions, tree->AsOp(), stmt);

case GT_JTRUE:
if (block != nullptr)
{
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -9138,6 +9138,7 @@ class Compiler
GenTree* optAssertionProp_RelOp(ASSERT_VALARG_TP assertions, GenTree* tree, Statement* stmt, BasicBlock* block);
GenTree* optAssertionProp_Comma(ASSERT_VALARG_TP assertions, GenTree* tree, Statement* stmt);
GenTree* optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree* tree, Statement* stmt);
GenTree* optAssertionProp_KnownBitsSimplify(ASSERT_VALARG_TP assertions, GenTreeOp* tree, Statement* stmt);
GenTree* optAssertionPropGlobal_RelOp(ASSERT_VALARG_TP assertions,
GenTree* tree,
Statement* stmt,
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/jit/jitconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ CONFIG_INTEGER(JitHideAlignBehindJmp, "JitHideAlignBehindJmp", 1)
CONFIG_INTEGER(JitOptimizeStructHiddenBuffer, "JitOptimizeStructHiddenBuffer", 1)
RELEASE_CONFIG_INTEGER(JitEnableStoreLclFldCoalescing, "JitEnableStoreLclFldCoalescing", 1)

// Enables the KnownBits (LLVM-style known zeros/ones) analysis and the assertion-prop optimizations
// that consume it. Set to 0 to disable.
RELEASE_CONFIG_INTEGER(JitEnableKnownBits, "JitEnableKnownBits", 1)

CONFIG_INTEGER(JitUnrollLoopMaxIterationCount,
"JitUnrollLoopMaxIterationCount",
DEFAULT_UNROLL_LOOP_MAX_ITERATION_COUNT)
Expand Down
Loading
Loading