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
48 changes: 48 additions & 0 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 @@ -4083,6 +4084,22 @@ 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). Covers TYP_LONG and bit patterns an interval cannot express (e.g. "x & 7").
if (!*isKnownNonZero || !*isKnownNonNegative)
{
const uint64_t signBit = 1ull << ((genTypeSize(genActualType(tree)) * BITS_PER_BYTE) - 1);
const KnownBits kb = KnownBits::Compute(this, treeVN, assertions);
if ((kb.knownZero & signBit) != 0)
{
*isKnownNonNegative = true;
}
if (kb.knownOne != 0)
{
*isKnownNonZero = true;
}
}
}

//------------------------------------------------------------------------
Expand Down Expand Up @@ -4514,6 +4531,23 @@ 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.
if (varTypeIsIntegral(op1) && (op1VN != ValueNumStore::NoVN) && (op2VN != ValueNumStore::NoVN))
{
const unsigned width = genTypeSize(genActualType(op1)) * BITS_PER_BYTE;
const KnownBits kb1 = KnownBits::Compute(this, op1VN, assertions);
Comment on lines +4534 to +4539
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 @@ -5537,6 +5571,20 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree
return optAssertionProp_Update(newTree, arrBndsChk, stmt);
};

// Known-bits elimination: redundant if (uint)index is provably < (uint)length. Catches masked
// indices and bit patterns the range-based paths cannot express. On 64-bit targets the index
// and length can both be TYP_I_IMPL (see fgMorphIndexAddr), so derive the width from the actual
// operand type instead of hardcoding 32 -- otherwise we'd discard the high 32 bits of a native-int
// index and could prove a (uint)idx < (uint)len fact that doesn't hold for the full value.
assert(genActualType(arrBndsChk->GetIndex()) == genActualType(arrBndsChk->GetArrayLength()));
const unsigned width = genTypeSize(genActualType(arrBndsChk->GetIndex())) * BITS_PER_BYTE;
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, width) == 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
282 changes: 282 additions & 0 deletions src/coreclr/jit/knownbits.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include "jitpch.h"
#ifdef _MSC_VER
#pragma hdrstop
#endif

#include "knownbits.h"

//------------------------------------------------------------------------
// MergeKnownBitsAssertions: Refine "*pBits" using whatever the live assertions tell us about "num".
//
// Arguments:
// comp - the compiler context
// num - the value number being analyzed
// assertions - the assertion set live at the consumer
// width - bit width (32 or 64) of "num"
// budget - recursive search budget (currently unused here, kept for symmetry with Compute)
// pBits - in/out: the lattice for "num" so far; refined in place by intersecting with each
// fact this routine can extract from the assertion table
//
static void MergeKnownBitsAssertions(
Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, unsigned width, int /*budget*/, KnownBits* pBits)
{
if (BitVecOps::MayBeUninit(assertions) || BitVecOps::IsEmpty(comp->apTraits, assertions) ||
!comp->optAssertionHasAssertionsForVN(num))
{
return;
}

const uint64_t signBit = 1ull << (width - 1);
const KnownBits signBitZero(signBit, 0);

// Tightest signed upper bound "num <= signedUpperBound" gathered from signed "num < C" / "num <= C"
// assertions with a non-negative bound. On its own a signed upper bound says nothing about the high
// bits (num could be negative), so we only apply it after the loop, and only once we also know num
// is non-negative (sign bit 0) -- then num is in [0, signedUpperBound] and its upper bits are 0.
bool haveSignedUpperBound = false;
uint64_t signedUpperBound = 0;

BitVecOps::Iter iter(comp->apTraits, assertions);
unsigned index = 0;
while (iter.NextElem(&index))
{
const Compiler::AssertionDsc& cur = comp->optGetAssertion(GetAssertionIndex(index));
if (cur.GetOp1().GetVN() != num)
{
continue;
}

// "num == const": fully determines the bits.
if (cur.KindIs(Compiler::OAK_EQUAL))
{
int64_t eqCns;
if (comp->vnStore->IsVNIntegralConstant<int64_t>(cur.GetOp2().GetVN(), &eqCns))
{
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromConstant((uint64_t)eqCns, width));
}
continue;
}

// Relops of the form "num <relop> const".
if (cur.IsRelop() && cur.GetOp2().KindIs(Compiler::O2K_CONST_INT))
{
const int64_t relCns = cur.GetOp2().GetIntConstant();

if (cur.KindIs(Compiler::OAK_LT_UN) && (relCns > 0))
{
// (uint)num < C => num u<= C-1 => upper bits are 0.
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound((uint64_t)(relCns - 1), width));
}
else if (cur.KindIs(Compiler::OAK_LE_UN) && (relCns >= 0))
{
// (uint)num <= C => upper bits are 0.
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound((uint64_t)relCns, width));
}
else if (cur.KindIs(Compiler::OAK_GE) && (relCns >= 0))
{
// num >= 0 (signed) => sign bit is 0.
*pBits = KnownBits::Intersect(*pBits, signBitZero);
}
else if (cur.KindIs(Compiler::OAK_GT) && (relCns >= -1))
{
// num > -1 (signed) => num >= 0 => sign bit is 0.
*pBits = KnownBits::Intersect(*pBits, signBitZero);
}
else if (cur.KindIs(Compiler::OAK_LT) && (relCns >= 1))
{
// num < C (signed), C >= 1. If num is also non-negative (handled after the loop),
// num is in [0, C-1], so record C-1 as a candidate upper bound.
const uint64_t ub = (uint64_t)(relCns - 1);
if (!haveSignedUpperBound || (ub < signedUpperBound))
{
haveSignedUpperBound = true;
signedUpperBound = ub;
}
}
else if (cur.KindIs(Compiler::OAK_LE) && (relCns >= 0))
{
// num <= C (signed), C >= 0. If num is also non-negative, num is in [0, C].
const uint64_t ub = (uint64_t)relCns;
if (!haveSignedUpperBound || (ub < signedUpperBound))
{
haveSignedUpperBound = true;
signedUpperBound = ub;
}
}
continue;
}

// "(uint)num </<= (vn + cns)" where (vn + cns) is non-negative => num is non-negative.
//
// IsVNNeverNegative on an O2K_VN_ADD_CNS asserts only that the "vn" part is non-negative.
// The full expression "vn + cns" can only be guaranteed non-negative when cns == 0, so we
// require it explicitly here -- otherwise a negative cns could make the bound itself
// negative and we'd derive a false non-negativity fact for num. Same shape as rangecheck.cpp.
//
if (cur.KindIs(Compiler::OAK_LT_UN, Compiler::OAK_LE_UN) && cur.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) &&
cur.GetOp2().IsVNNeverNegative() && (cur.GetOp2().GetCns() == 0))
{
*pBits = KnownBits::Intersect(*pBits, signBitZero);
}
}

// If we gathered a signed upper bound and num is now known non-negative (from any of the facts
// above or from its value-number structure), num is in [0, signedUpperBound]: its upper bits are 0.
// Example: "a > 10 && a < 1000" => sign bit 0 (from a > 10) plus upper bits 0 (from a < 1000),
// proving a fits in a smaller type (e.g. making "checked((int)a)" non-overflowing).
if (haveSignedUpperBound && ((pBits->knownZero & signBit) != 0))
{
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound(signedUpperBound, width));
}
}

//------------------------------------------------------------------------
// ComputeWorker: Recursive worker for KnownBits::Compute.
//
// Arguments:
// comp - the compiler context
// num - the value number to analyze
// assertions - the assertion set live at the consumer
// budget - recursive search budget; decremented at every recursive step. Returns the
// fully-unknown lattice when the budget is exhausted.
// visited - set of phi VNs we have already entered, used to guard against infinite recursion
// on loop-carried phis
//
// Returns:
// KnownBits for "num" within its natural width (32 or 64). Always truncated to that width on
// return so the "bits above width are 0/0" invariant holds.
//
static KnownBits ComputeWorker(
Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget, ValueNumStore::SmallValueNumSet* visited)
{
KnownBits result;
if ((num == ValueNumStore::NoVN) || (budget <= 0))
{
return result;
}

const var_types vnType = comp->vnStore->TypeOfVN(num);
if (!varTypeIsIntegral(vnType) || varTypeIsGC(vnType))
{
// We only reason about (non-GC) integral values.
return result;
}

const unsigned width = (genActualType(vnType) == TYP_LONG) ? 64 : 32;

// Constants are fully known.
int64_t cnsVal;
if (comp->vnStore->IsVNIntegralConstant<int64_t>(num, &cnsVal))
{
return KnownBits::FromConstant((uint64_t)cnsVal, width);
}

VNFuncApp f;
if (comp->vnStore->GetVNFunc(num, &f))
{
switch (f.GetFunc())
{
case VNF_AND:
case VNF_OR:
case VNF_UDIV:
{
const KnownBits a = ComputeWorker(comp, f.GetArg(0), assertions, --budget, visited);
const KnownBits b = ComputeWorker(comp, f.GetArg(1), assertions, --budget, visited);

if (f.FuncIs(VNF_UDIV))
result = KnownBitsOps::UDiv(a, b, width);
else if (f.FuncIs(VNF_AND))
result = KnownBitsOps::And(a, b);
else if (f.FuncIs(VNF_OR))
result = KnownBitsOps::Or(a, b);
else
unreached();
break;
}

case VNF_Cast:
case VNF_CastOvf:
{
var_types castToType;
bool srcIsUnsigned;
comp->vnStore->GetCastOperFromVN(f.GetArg(1), &castToType, &srcIsUnsigned);

const ValueNum srcVN = f.GetArg(0);
const var_types srcType = comp->vnStore->TypeOfVN(srcVN);
if (varTypeIsIntegral(srcType) && !varTypeIsGC(srcType) && varTypeIsIntegral(castToType))
{
const unsigned srcWidth = genTypeSize(genActualType(srcType)) * BITS_PER_BYTE;
const KnownBits bits = ComputeWorker(comp, srcVN, assertions, --budget, visited);
result = KnownBitsOps::Cast(bits, srcWidth, castToType, srcIsUnsigned);
}
break;
}

case VNF_EQ:
case VNF_NE:
case VNF_LT:
case VNF_LE:
case VNF_GT:
case VNF_GE:
case VNF_LT_UN:
case VNF_LE_UN:
case VNF_GT_UN:
case VNF_GE_UN:
// A relop always produces 0 or 1; we don't try to fold the comparison here, just
// record the [0, 1] range so a consumer reading this VN sees a single low bit.
result = KnownBits::FromUnsignedUpperBound(1, width);
break;

case VNF_MDARR_LENGTH:
case VNF_ARR_LENGTH:
// Array length is in [0, CORINFO_Array_MaxLength], so its upper bits are 0.
result = KnownBits::FromUnsignedUpperBound(CORINFO_Array_MaxLength, width);
break;

default:
break;
}
}

result = result.Truncate(width);

// Phi: a bit is known in the phi result only if it is known and equal along every reaching
// edge. We Union (LLVM's intersectWith) the per-edge KnownBits to compute that.
if (!result.IsConstant(width) && comp->vnStore->IsPhiDef(num) && visited->Add(comp, num))
{
KnownBits phiBits;
bool first = true;
auto visitor = [comp, &phiBits, &first, &budget, visited](ValueNum vn, ASSERT_TP reachAss) {
const KnownBits edge = ComputeWorker(comp, vn, reachAss, --budget, visited);
phiBits = first ? edge : KnownBits::Union(phiBits, edge);
first = false;

// Once nothing is known, merging more edges cannot recover any information.
return phiBits.IsUnknown() ? Compiler::AssertVisit::Abort : Compiler::AssertVisit::Continue;
};
if ((comp->optVisitReachingAssertions(num, visitor) == Compiler::AssertVisit::Continue) && !first)
{
result = KnownBits::Intersect(result, phiBits);
}
}

MergeKnownBitsAssertions(comp, num, assertions, width, budget, &result);
return result.Truncate(width);
}

//------------------------------------------------------------------------
// KnownBits::Compute: Entry point for the bit-level analog of
// RangeCheck::GetRangeFromAssertions. Returns which bits of "num" are known 0/1, derived from
// its value-number structure and the incoming assertions. Supports 32- and 64-bit integral VNs;
// on unsupported types returns the fully-unknown lattice.
//
// See KnownBits::Compute in knownbits.h for the parameter documentation.
//
KnownBits KnownBits::Compute(Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget)
{
ValueNumStore::SmallValueNumSet visited;
return ComputeWorker(comp, num, assertions, budget, &visited);
}
Loading
Loading