Skip to content

Commit c19b73a

Browse files
EgorBoCopilot
andcommitted
JIT: Add KnownBits (LLVM-style known zeros/ones) analysis
Adds a 32/64-bit fixed-width KnownBits lattice for integral VNs, plus an assertion-aware analysis (KnownBits::Compute) that derives the known bits of a value number from its VN structure and the incoming assertions. The struct and transfer functions are ports of llvm::KnownBits from llvm/Support/KnownBits.{h,cpp}; only the subset of operations needed by the current consumers (And/Or/UDiv/Cast/EvalRelop) is included. Three consumers wired in assertionprop.cpp: * optAssertionProp_RangeProperties - sign-bit/non-zero from KnownBits * optAssertionPropGlobal_RelOp - relop folding * optAssertionProp_BndsChk - (uint)index < (uint)length Plus a one-shot KnownBits refinement at the end of RangeCheck::GetRangeFromAssertionsWorker that tightens the signed [lo, hi] when an interval side is at the type extreme. libraries.pmi: -14,515 / +6 bytes (489 contexts, 409 size improvements, 2 regressions). libraries_tests.run: -246k / +0.4k bytes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ecaa1c5 commit c19b73a

5 files changed

Lines changed: 689 additions & 0 deletions

File tree

src/coreclr/jit/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ set( JIT_SOURCES
152152
jiteh.cpp
153153
jithashtable.cpp
154154
jitmetadata.cpp
155+
knownbits.cpp
155156
layout.cpp
156157
lclmorph.cpp
157158
lclvars.cpp
@@ -378,6 +379,7 @@ set( JIT_HEADERS
378379
jitmetadatalist.h
379380
jitpch.h
380381
jitstd.h
382+
knownbits.h
381383
lir.h
382384
loopcloning.h
383385
loopcloningopts.h

src/coreclr/jit/assertionprop.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1212

1313
#include "jitpch.h"
1414
#include "rangecheck.h"
15+
#include "knownbits.h"
1516
#ifdef _MSC_VER
1617
#pragma hdrstop
1718
#endif
@@ -4083,6 +4084,23 @@ void Compiler::optAssertionProp_RangeProperties(ASSERT_VALARG_TP assertions,
40834084
*isKnownNonZero = true;
40844085
}
40854086
}
4087+
4088+
// Known bits can also establish non-negativity (sign bit known 0) and non-zeroness (some bit
4089+
// known 1). Covers TYP_LONG and bit patterns an interval cannot express (e.g. "x & 7").
4090+
if (!*isKnownNonZero || !*isKnownNonNegative)
4091+
{
4092+
const unsigned width = (genActualType(tree) == TYP_LONG) ? 64 : 32;
4093+
const uint64_t signBit = 1ull << (width - 1);
4094+
const KnownBits kb = KnownBits::Compute(this, treeVN, assertions);
4095+
if ((kb.knownZero & signBit) != 0)
4096+
{
4097+
*isKnownNonNegative = true;
4098+
}
4099+
if (kb.knownOne != 0)
4100+
{
4101+
*isKnownNonZero = true;
4102+
}
4103+
}
40864104
}
40874105

40884106
//------------------------------------------------------------------------
@@ -4514,6 +4532,23 @@ GenTree* Compiler::optAssertionPropGlobal_RelOp(ASSERT_VALARG_TP assertions,
45144532
}
45154533
}
45164534

4535+
// See if we can fold the relop based on known bits. This complements the range-based folding
4536+
// above (which is limited to TYP_INT) by reasoning about individual bits and TYP_LONG values.
4537+
if (varTypeIsIntegral(op1) && !varTypeIsGC(op1) && (op1VN != ValueNumStore::NoVN) && (op2VN != ValueNumStore::NoVN))
4538+
{
4539+
const unsigned width = (genActualType(op1) == TYP_LONG) ? 64 : 32;
4540+
const KnownBits kb1 = KnownBits::Compute(this, op1VN, assertions);
4541+
const KnownBits kb2 = KnownBits::Compute(this, op2VN, assertions);
4542+
4543+
const int relopResult = KnownBitsOps::EvalRelop(tree->OperGet(), tree->IsUnsigned(), kb1, kb2, width);
4544+
if (relopResult >= 0)
4545+
{
4546+
JITDUMP("Folding relop [%06u] based on known bits.\n", dspTreeID(tree));
4547+
newTree = gtWrapWithSideEffects(relopResult == 1 ? gtNewTrue() : gtNewFalse(), tree, GTF_ALL_EFFECT);
4548+
return optAssertionProp_Update(newTree, tree, stmt);
4549+
}
4550+
}
4551+
45174552
// Else check if we have an equality check involving a local or an indir
45184553
if (!tree->OperIs(GT_EQ, GT_NE))
45194554
{
@@ -5537,6 +5572,17 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree
55375572
return optAssertionProp_Update(newTree, arrBndsChk, stmt);
55385573
};
55395574

5575+
// Known-bits elimination: redundant if (uint)index is provably < (uint)length. Catches masked
5576+
// indices and bit patterns the range-based paths cannot express.
5577+
{
5578+
const KnownBits kbIdx = KnownBits::Compute(this, vnCurIdx, assertions);
5579+
const KnownBits kbLen = KnownBits::Compute(this, vnCurLen, assertions);
5580+
if (KnownBitsOps::EvalRelop(GT_LT, /* isUnsigned */ true, kbIdx, kbLen, 32) == 1)
5581+
{
5582+
return dropBoundsCheck(INDEBUG("known bits prove (uint)index < (uint)length"));
5583+
}
5584+
}
5585+
55405586
// First, check if we have arr[arr.Length - cns] when we know arr.Length is >= cns.
55415587
ValueNum add0, add1;
55425588
if (vnStore->IsVNBinFunc(vnCurIdx, VNF_ADD, &add0, &add1))

src/coreclr/jit/knownbits.cpp

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#include "jitpch.h"
5+
#ifdef _MSC_VER
6+
#pragma hdrstop
7+
#endif
8+
9+
#include "knownbits.h"
10+
11+
//------------------------------------------------------------------------
12+
// MergeKnownBitsAssertions: Refine "*pBits" using whatever the live assertions tell us about "num".
13+
//
14+
// Arguments:
15+
// comp - the compiler context
16+
// num - the value number being analyzed
17+
// assertions - the assertion set live at the consumer
18+
// width - bit width (32 or 64) of "num"
19+
// budget - recursive search budget (currently unused here, kept for symmetry with Compute)
20+
// pBits - in/out: the lattice for "num" so far; refined in place by intersecting with each
21+
// fact this routine can extract from the assertion table
22+
//
23+
static void MergeKnownBitsAssertions(
24+
Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, unsigned width, int /*budget*/, KnownBits* pBits)
25+
{
26+
if (BitVecOps::MayBeUninit(assertions) || BitVecOps::IsEmpty(comp->apTraits, assertions) ||
27+
!comp->optAssertionHasAssertionsForVN(num))
28+
{
29+
return;
30+
}
31+
32+
const uint64_t signBit = 1ull << (width - 1);
33+
const KnownBits signBitZero(signBit, 0);
34+
35+
// Tightest signed upper bound "num <= signedUpperBound" gathered from signed "num < C" / "num <= C"
36+
// assertions with a non-negative bound. On its own a signed upper bound says nothing about the high
37+
// bits (num could be negative), so we only apply it after the loop, and only once we also know num
38+
// is non-negative (sign bit 0) -- then num is in [0, signedUpperBound] and its upper bits are 0.
39+
bool haveSignedUpperBound = false;
40+
uint64_t signedUpperBound = 0;
41+
42+
BitVecOps::Iter iter(comp->apTraits, assertions);
43+
unsigned index = 0;
44+
while (iter.NextElem(&index))
45+
{
46+
const Compiler::AssertionDsc& cur = comp->optGetAssertion(GetAssertionIndex(index));
47+
if (cur.GetOp1().GetVN() != num)
48+
{
49+
continue;
50+
}
51+
52+
// "num == const": fully determines the bits.
53+
if (cur.KindIs(Compiler::OAK_EQUAL))
54+
{
55+
int64_t eqCns;
56+
if (comp->vnStore->IsVNIntegralConstant<int64_t>(cur.GetOp2().GetVN(), &eqCns))
57+
{
58+
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromConstant((uint64_t)eqCns, width));
59+
}
60+
continue;
61+
}
62+
63+
// Relops of the form "num <relop> const".
64+
if (cur.IsRelop() && cur.GetOp2().KindIs(Compiler::O2K_CONST_INT))
65+
{
66+
const int64_t relCns = cur.GetOp2().GetIntConstant();
67+
68+
if (cur.KindIs(Compiler::OAK_LT_UN) && (relCns > 0))
69+
{
70+
// (uint)num < C => num u<= C-1 => upper bits are 0.
71+
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound((uint64_t)(relCns - 1), width));
72+
}
73+
else if (cur.KindIs(Compiler::OAK_LE_UN) && (relCns >= 0))
74+
{
75+
// (uint)num <= C => upper bits are 0.
76+
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound((uint64_t)relCns, width));
77+
}
78+
else if (cur.KindIs(Compiler::OAK_GE) && (relCns >= 0))
79+
{
80+
// num >= 0 (signed) => sign bit is 0.
81+
*pBits = KnownBits::Intersect(*pBits, signBitZero);
82+
}
83+
else if (cur.KindIs(Compiler::OAK_GT) && (relCns >= -1))
84+
{
85+
// num > -1 (signed) => num >= 0 => sign bit is 0.
86+
*pBits = KnownBits::Intersect(*pBits, signBitZero);
87+
}
88+
else if (cur.KindIs(Compiler::OAK_LT) && (relCns >= 1))
89+
{
90+
// num < C (signed), C >= 1. If num is also non-negative (handled after the loop),
91+
// num is in [0, C-1], so record C-1 as a candidate upper bound.
92+
const uint64_t ub = (uint64_t)(relCns - 1);
93+
if (!haveSignedUpperBound || (ub < signedUpperBound))
94+
{
95+
haveSignedUpperBound = true;
96+
signedUpperBound = ub;
97+
}
98+
}
99+
else if (cur.KindIs(Compiler::OAK_LE) && (relCns >= 0))
100+
{
101+
// num <= C (signed), C >= 0. If num is also non-negative, num is in [0, C].
102+
const uint64_t ub = (uint64_t)relCns;
103+
if (!haveSignedUpperBound || (ub < signedUpperBound))
104+
{
105+
haveSignedUpperBound = true;
106+
signedUpperBound = ub;
107+
}
108+
}
109+
continue;
110+
}
111+
112+
// "(uint)num </<= (vn + cns)" where (vn + cns) is non-negative => num is non-negative.
113+
//
114+
// IsVNNeverNegative on an O2K_VN_ADD_CNS asserts only that the "vn" part is non-negative.
115+
// The full expression "vn + cns" can only be guaranteed non-negative when cns == 0, so we
116+
// require it explicitly here -- otherwise a negative cns could make the bound itself
117+
// negative and we'd derive a false non-negativity fact for num. Same shape as rangecheck.cpp.
118+
//
119+
if (cur.KindIs(Compiler::OAK_LT_UN, Compiler::OAK_LE_UN) && cur.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) &&
120+
cur.GetOp2().IsVNNeverNegative() && (cur.GetOp2().GetCns() == 0))
121+
{
122+
*pBits = KnownBits::Intersect(*pBits, signBitZero);
123+
}
124+
}
125+
126+
// If we gathered a signed upper bound and num is now known non-negative (from any of the facts
127+
// above or from its value-number structure), num is in [0, signedUpperBound]: its upper bits are 0.
128+
// Example: "a > 10 && a < 1000" => sign bit 0 (from a > 10) plus upper bits 0 (from a < 1000),
129+
// proving a fits in a smaller type (e.g. making "checked((int)a)" non-overflowing).
130+
if (haveSignedUpperBound && ((pBits->knownZero & signBit) != 0))
131+
{
132+
*pBits = KnownBits::Intersect(*pBits, KnownBits::FromUnsignedUpperBound(signedUpperBound, width));
133+
}
134+
}
135+
136+
//------------------------------------------------------------------------
137+
// ComputeWorker: Recursive worker for KnownBits::Compute.
138+
//
139+
// Arguments:
140+
// comp - the compiler context
141+
// num - the value number to analyze
142+
// assertions - the assertion set live at the consumer
143+
// budget - recursive search budget; decremented at every recursive step. Returns the
144+
// fully-unknown lattice when the budget is exhausted.
145+
// visited - set of phi VNs we have already entered, used to guard against infinite recursion
146+
// on loop-carried phis
147+
//
148+
// Returns:
149+
// KnownBits for "num" within its natural width (32 or 64). Always truncated to that width on
150+
// return so the "bits above width are 0/0" invariant holds.
151+
//
152+
static KnownBits ComputeWorker(
153+
Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget, ValueNumStore::SmallValueNumSet* visited)
154+
{
155+
KnownBits result;
156+
if ((num == ValueNumStore::NoVN) || (budget <= 0))
157+
{
158+
return result;
159+
}
160+
161+
const var_types vnType = comp->vnStore->TypeOfVN(num);
162+
if (!varTypeIsIntegral(vnType) || varTypeIsGC(vnType))
163+
{
164+
// We only reason about (non-GC) integral values.
165+
return result;
166+
}
167+
168+
const unsigned width = (genActualType(vnType) == TYP_LONG) ? 64 : 32;
169+
170+
// Constants are fully known.
171+
int64_t cnsVal;
172+
if (comp->vnStore->IsVNIntegralConstant<int64_t>(num, &cnsVal))
173+
{
174+
return KnownBits::FromConstant((uint64_t)cnsVal, width);
175+
}
176+
177+
VNFuncApp f;
178+
if (comp->vnStore->GetVNFunc(num, &f))
179+
{
180+
switch (f.GetFunc())
181+
{
182+
case VNF_AND:
183+
case VNF_OR:
184+
case VNF_UDIV:
185+
{
186+
const KnownBits a = ComputeWorker(comp, f.GetArg(0), assertions, --budget, visited);
187+
const KnownBits b = ComputeWorker(comp, f.GetArg(1), assertions, --budget, visited);
188+
189+
if (f.FuncIs(VNF_UDIV))
190+
result = KnownBitsOps::UDiv(a, b, width);
191+
else if (f.FuncIs(VNF_AND))
192+
result = KnownBitsOps::And(a, b);
193+
else if (f.FuncIs(VNF_OR))
194+
result = KnownBitsOps::Or(a, b);
195+
else
196+
unreached();
197+
break;
198+
}
199+
200+
case VNF_Cast:
201+
case VNF_CastOvf:
202+
{
203+
var_types castToType;
204+
bool srcIsUnsigned;
205+
comp->vnStore->GetCastOperFromVN(f.GetArg(1), &castToType, &srcIsUnsigned);
206+
207+
const ValueNum srcVN = f.GetArg(0);
208+
const var_types srcType = comp->vnStore->TypeOfVN(srcVN);
209+
if (varTypeIsIntegral(srcType) && !varTypeIsGC(srcType) && varTypeIsIntegral(castToType))
210+
{
211+
const unsigned srcWidth = genTypeSize(genActualType(srcType)) * BITS_PER_BYTE;
212+
const KnownBits bits = ComputeWorker(comp, srcVN, assertions, --budget, visited);
213+
result = KnownBitsOps::Cast(bits, srcWidth, castToType, srcIsUnsigned);
214+
}
215+
break;
216+
}
217+
218+
case VNF_EQ:
219+
case VNF_NE:
220+
case VNF_LT:
221+
case VNF_LE:
222+
case VNF_GT:
223+
case VNF_GE:
224+
case VNF_LT_UN:
225+
case VNF_LE_UN:
226+
case VNF_GT_UN:
227+
case VNF_GE_UN:
228+
// A relop always produces 0 or 1; we don't try to fold the comparison here, just
229+
// record the [0, 1] range so a consumer reading this VN sees a single low bit.
230+
result = KnownBits::FromUnsignedUpperBound(1, width);
231+
break;
232+
233+
case VNF_MDARR_LENGTH:
234+
case VNF_ARR_LENGTH:
235+
// Array length is in [0, CORINFO_Array_MaxLength], so its upper bits are 0.
236+
result = KnownBits::FromUnsignedUpperBound(CORINFO_Array_MaxLength, width);
237+
break;
238+
239+
default:
240+
break;
241+
}
242+
}
243+
244+
result = result.Truncate(width);
245+
246+
// Phi: a bit is known in the phi result only if it is known and equal along every reaching
247+
// edge. We Union (LLVM's intersectWith) the per-edge KnownBits to compute that.
248+
if (!result.IsConstant(width) && comp->vnStore->IsPhiDef(num) && visited->Add(comp, num))
249+
{
250+
KnownBits phiBits;
251+
bool first = true;
252+
auto visitor = [comp, &phiBits, &first, &budget, visited](ValueNum vn, ASSERT_TP reachAss) {
253+
const KnownBits edge = ComputeWorker(comp, vn, reachAss, --budget, visited);
254+
phiBits = first ? edge : KnownBits::Union(phiBits, edge);
255+
first = false;
256+
257+
// Once nothing is known, merging more edges cannot recover any information.
258+
return phiBits.IsUnknown() ? Compiler::AssertVisit::Abort : Compiler::AssertVisit::Continue;
259+
};
260+
if ((comp->optVisitReachingAssertions(num, visitor) == Compiler::AssertVisit::Continue) && !first)
261+
{
262+
result = KnownBits::Intersect(result, phiBits);
263+
}
264+
}
265+
266+
MergeKnownBitsAssertions(comp, num, assertions, width, budget, &result);
267+
return result.Truncate(width);
268+
}
269+
270+
//------------------------------------------------------------------------
271+
// KnownBits::Compute: Entry point for the bit-level analog of
272+
// RangeCheck::GetRangeFromAssertions. Returns which bits of "num" are known 0/1, derived from
273+
// its value-number structure and the incoming assertions. Supports 32- and 64-bit integral VNs;
274+
// on unsupported types returns the fully-unknown lattice.
275+
//
276+
// See KnownBits::Compute in knownbits.h for the parameter documentation.
277+
//
278+
KnownBits KnownBits::Compute(Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget)
279+
{
280+
ValueNumStore::SmallValueNumSet visited;
281+
return ComputeWorker(comp, num, assertions, budget, &visited);
282+
}

0 commit comments

Comments
 (0)