JIT: Add RangeOps::Xor and RangeOps::Not#126553
JIT: Add RangeOps::Xor and RangeOps::Not#126553BoyBaykiller wants to merge 6 commits intodotnet:mainfrom
RangeOps::Xor and RangeOps::Not#126553Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
CI is down so we can't see the diffs.
um.. I don't see XOR there - does it appear there after some jit transformation? |
There was a problem hiding this comment.
Pull request overview
This PR updates CoreCLR JIT range analysis to understand xor so that range tightening (and downstream optimizations like branch removal) keep working after the CONST - x → xor x, CONST transformation from #126529.
Changes:
- Add
RangeOps::Xorto compute ranges for a limited set of XOR patterns (constant-folding and^ -1/^ INT_MAX). - Teach
RangeCheck::GetRangeFromAssertionsto compute XOR ranges from VN assertions (VNF_XOR). - Use
RangeOps::XorinRangeCheck::ComputeRangeForBinOpforGT_XOR(while preserving the existing Log2 special-case).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/coreclr/jit/rangecheck.h |
Adds RangeOps::Xor to enable tighter range inference for certain XOR cases. |
src/coreclr/jit/rangecheck.cpp |
Wires XOR into assertion-based range derivation and general binop range computation. |
| // x ^ -1 is equivalent to -1 - x | ||
| // x ^ INT_MAX is equivalent to INT_MAX - x | ||
| // Example: [3..5] ^ [-1..-1] = [-6..-4] | ||
| if (r1IsConstVal && (r1ConstVal == -1 || r1ConstVal == INT_MAX)) | ||
| { | ||
| return Subtract(r1, r2); | ||
| } |
There was a problem hiding this comment.
RangeOps::Xor maps the ^ -1 case to Subtract, but Subtract delegates through Negate which returns unknown for constant ranges that include INT_MIN. Since XOR/bitwise-NOT has no overflow semantics, this unnecessarily loses precision for ranges like [INT_MIN..k] ^ [-1..-1]. Consider handling the -1 case directly (e.g., by computing bitwise-NOT of constant limits) so it can still produce a constant range when the input range is constant.
| { | ||
| return Subtract(r2, r1); | ||
| } | ||
|
|
There was a problem hiding this comment.
Remove the trailing whitespace on this blank line to satisfy repo formatting/linting expectations.
| case GT_XOR: | ||
| r = RangeOps::Xor(op1Range, op2Range); | ||
| break; |
There was a problem hiding this comment.
This change is meant to prevent a performance regression by allowing range tightening through XOR (e.g., x ^ int.MaxValue) again. Please add a JIT regression test (likely under src/tests/JIT/opt/RangeChecks or similar) that fails without this change and verifies the relevant optimization (e.g., absence of the throw helper / simplified control flow) so future range-analysis tweaks don’t reintroduce the regression.
Yes it gets produced by my other PR which I linked. |
You're touching code |
well, then for now it's just a bunch of untested/never-triggered code without tests? It looks correct to me, although, I think we should make Xor handling a bit smarter than focusing only on -1/maxvalue constants. E.g. the Log2 pattern involves ^ 31/^ 63 - if we can remove special handling for log2 and recognize it naturally.. |
Well my hope is that the other PR gets merged first soon (which is why this is draft for now)
Yeah that'd be nice. But it seems like XOR alone is not enough to handle that. So maybe for an other PR? Although let me see if xor has a more general pattern |
* add Not too
…ssion in System.Data.SqlTypes.SqlDecimal:AdjustScale 114664 libraries.pmi * add Not handling to ComputeRange
RangeOps::XorRangeOps::Xor and RangeOps::Not
Given any `CONST - x` and range info for `x` obtained from `IntegralRange::ForNode` see if we can transform it to `x ^ CONST`. These changes motivate improvements in `IntegralRange::ForNode` and `RangeOps` (#126553). It also made me find a missing `GT_NOT` in asserprop to enable VN based folding. Found by looking at diffs from #126442 Example: ```cs int Byte255(byte x) { return 255 - x; } ``` ```assembly ;; ------ BASE G_M64250_IG02: ;; offset=0x0000 movzx rax, dl neg eax add eax, 255 ;; size=10 bbWeight=1 PerfScore 0.75 G_M64250_IG03: ;; offset=0x000A ret ;; size=1 bbWeight=1 PerfScore 1.00 ;; ------ DIFF G_M64250_IG02: ;; offset=0x0000 movzx rax, dl xor eax, 255 ;; size=8 bbWeight=1 PerfScore 0.50 G_M64250_IG03: ;; offset=0x0008 ret ;; size=1 bbWeight=1 PerfScore 1.00 ``` ```cs int IntMin1(int x) { return -1 - x; } ``` ```assembly ;; ------ BASE G_M50835_IG02: ;; offset=0x0000 mov eax, edx neg eax dec eax ;; size=6 bbWeight=1 PerfScore 0.75 G_M50835_IG03: ;; offset=0x0006 ret ;; size=1 bbWeight=1 PerfScore 1.00 ;; ------ DIFF G_M50835_IG02: ;; offset=0x0000 mov eax, edx not eax ;; size=4 bbWeight=1 PerfScore 0.50 G_M50835_IG03: ;; offset=0x0004 ret ;; size=1 bbWeight=1 PerfScore 1.00 ``` --------- Co-authored-by: Egor Bogatov <egorbo@gmail.com>
…-handling-to-rangecheck
|
The XOR handling here is very general, but that's not enough to catch the log2 pattern of Here is an example showing that XOR can deal with it: void Test(int value)
{
value |= 1;
value = (int)Lzcnt.LeadingZeroCount((uint)value);
// Range for value must be [0, 31] but rangecheck doesn't
// know so let's help it with this branch
if (value >= 0 && value <= 31)
{
// Example array from https://github.com/dotnet/runtime/pull/113790
ReadOnlySpan<byte> log2ToPow10 = stackalloc byte[32];
// New! No bounds check, it sees XOR is equivalent to SUB here
int index = 31 ^ value;
_ = log2ToPow10[index];
}
}It shouldnt be too hard to implement better range for LZCNT(x | 1) on 32 bit (using the |
|
@EgorBo PTAL. |
| return result; | ||
| } | ||
|
|
||
| static Range Not(const Range& range) |
There was a problem hiding this comment.
last I checked Not produced 0 diffs, so we'd better remove it
There was a problem hiding this comment.
#126553 (comment) Here I was talking about 2 large diffs in libraries_tests_no_tiered_compilation caused by NOT handling.
Additionally it fixes regressions from my recent transformation (#126529).
Example regression in libraries.pmi in SqlDecimal:op_Multiply (diffs):

Which this fixes by adding NOT handling (diffs):
![]()
| knownBits &= (1ULL << (sizeInBits - 1)) - 1; | ||
|
|
||
| bool noCarry = (cns & knownBits) == knownBits; | ||
| if (noCarry) |
There was a problem hiding this comment.
I just factored out the BitsetFromRange since we now use it in morph and rangecheck and adjusted the code a little. We can use BitsetFromRange to do other stuff for example transform ADD to OR if we wanted.
Fix multiple regression from #126529.
One example: ParallelEnumerable.Range.
Simplified: