Repository navigation
Modular range folds - #136994
Draft
NinoFloris wants to merge 14 commits into
Draft
Modular range folds#136994NinoFloris wants to merge 14 commits into
NinoFloris wants to merge 14 commits into
Conversation
Move the modular interval arithmetic of redundant branch opts into modularrange.h, a ring interval that models constant comparisons of either signedness, with symbolic upper bounds by identity. The redundant branch optimizer is converted to construct and intersect these intervals; it produces no code differences.
Replace GetIntersection, FoldRangeTests and FoldNeverNegativeRangeTest with a single FoldComparisons that intersects the two tests as modular intervals and emits the comparison form that matches the result. The conversion keeps the previously accepted shapes only: two tests of the same signedness on a plain local that enclose a range of at least two non-negative values, or "X >= 0" paired with a never-negative symbolic bound. It produces no code differences. The shapes the interval representation makes available, equality tests, negative bounds, offset bases and single values, are enabled separately.
…ools Accept equality tests, offset bases, field locals, mixed signedness, overlapping ranges and single values in the range test fold. The dominant new shape is "X != a && X != a + 1", which becomes a single unsigned comparison.
The previous fold rejected negative constants. On the modular interval they are ordinary points, so "lo <= X <= hi" with a negative lower bound becomes "(X - lo) u<= hi - lo" like any other range.
SUB/CMP needs as many arithmetic instructions as CMP/CCMP. Decline the offset form of a fold when its constants cost more than the two original bounds.
|
Azure Pipelines: Successfully started running 6 pipeline(s). 9 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
NinoFloris
force-pushed
the
jit/modular-range-folds
branch
from
November 5, 2026 04:13
0aaaeab to
166e5b7
Compare
The general case of the range fold lowered an interval as "(X - lower) u<= width" whatever its position on the ring. An interval that covers more than half of the ring is the complement of a small one, and one that ends near the top of the signed range is the signed test of its offset value, so the literal encoding materialized constants like 0xFFFFFFFD for "x != a && x != a + 1" and 0x7FFFFFF9 for "(uint)x < 257 || (x - 257) < 6". On arm64 the wrapped bound still encodes in a cmn while the compare stands alone, but once the compare joins a ccmp chain its 5-bit immediate forces a movn, which is where the folds grew code. An interval has four encodings: the unsigned test of itself or of its complement, and the signed test of either, which starts the interval at the minimum value. They differ only in their constants. Price each with the cost model and take the cheapest, with the smallest constants on a tie, since those fit the most immediate forms. The two shapes above become "(x - a) u> 1" and "(x - 257) <s 6".
…ptors The value number store and the assertion table are released after the SSA based phases, so a later phase that asks GenTree::IsNeverNegative about a local only sees the flag on its descriptor, which import and promotion set for span lengths and little else. A length read into a plain local is never negative by value number and nothing after value numbering could tell. Add a phase at the end of the SSA section that writes such facts onto the descriptors. The first is never-negative: a local in SSA whose every definition is never negative by value number gets the flag, where the implicit initial definition counts as zero when the method zero-initializes its locals and disqualifies the local otherwise. Optimize bools, if conversion, lowering and codegen then see array and span lengths, checked bounds and the temps CSE made for them the way the value number phases did.
A value that is never negative takes only the nonnegative half of the ring, but the range fold intersected its tests over the whole ring, so a pair like "(uint)length < 257 || (length - 257) < 6" on an array length produced an interval straddling the sign boundary and a signed test with an offset. Intersect the first test's interval with the nonnegative half when the value is never negative; the pair above is then "length < 263" with no arithmetic.
NinoFloris
force-pushed
the
jit/modular-range-folds
branch
from
November 5, 2026 04:24
166e5b7 to
61d264e
Compare
The range fold only took values compared at their own width, so a byte or char local compared against an int constant was not a candidate although the compare is an int compare. Accept the small-typed value, do the arithmetic at int width, and start its interval from what IntegralRange::ForNode knows about it: the type's bounds for a byte or char, the result range of a cast or a shift otherwise. The never-negative narrowing becomes one case of that. A byte local's "x != 255 && x > 99" is then the one interval [100, 254] instead of two tests.
The range fold required a local as the compared value, so two tests of the same field or array length only folded when CSE had already turned the repeated load into a local. Where CSE declines, the second block's load is the first's again, and nothing between them can store to it: the second block is a single statement with the first as its only predecessor. Accept a load as the base when it carries no persistent effects and is not volatile, keep the first test's load in place, and drop the second. The two loads differ only in that the second is marked non-faulting once the first has taken the null check, which GenTree::Compare treats as a different load; compare them by their address instead. The test disables CSE so the fold has to recognize the load itself.
Use the existing tree splitter to evaluate the first comparison's operands before combining the tests. This exposes pure comparisons when CSE embeds a definition in the first test, while retaining the second test's side-effect and cost restrictions.
NinoFloris
added a commit
to NinoFloris/dotnet-runtime
that referenced
this pull request
Nov 5, 2026
OperExceptions answered a signed division's overflow from the never-negative status of its operands, which reads the local descriptor, the integral range and the value number store, so the exception set of the node changed whenever the JIT learned something about an operand while its GTF_EXCEPT stayed what morph computed. A phase that records never-negative on locals after the SSA phases, in dotnet#136994, turns that into an extra flags assert in the checker on a signed division by such a local. OperExceptions now consults only the two GTF_DIV_MOD_NO flags and constant operands, and whoever proves an operand safe records it on the node: morph on every target instead of only arm64, loongarch64 and riscv64, assertion propagation as before, and the stack level setter before codegen, which already re-derived the flags at that point. Zero diffs on the osx-arm64 libraries pmi collection.
NinoFloris
force-pushed
the
jit/modular-range-folds
branch
from
November 5, 2026 12:59
f358648 to
9a57770
Compare
NinoFloris
added a commit
to NinoFloris/dotnet-runtime
that referenced
this pull request
Nov 5, 2026
OperExceptions answered a signed division's overflow from the never-negative status of its operands, which reads the local descriptor, the integral range and the value number store, so the exception set of the node changed whenever the JIT learned something about an operand while its GTF_EXCEPT stayed what morph computed. A phase that records never-negative on locals after the SSA phases, in dotnet#136994, turns that into an extra flags assert in the checker on a signed division by such a local. OperExceptions now consults only the two GTF_DIV_MOD_NO flags and constant operands, and whoever proves an operand safe records it on the node: morph on every target instead of only arm64, loongarch64 and riscv64, assertion propagation as before, and the stack level setter before codegen, which already re-derived the flags at that point. Zero diffs on the osx-arm64 libraries pmi collection.
NinoFloris
force-pushed
the
jit/modular-range-folds
branch
from
November 5, 2026 14:09
9a57770 to
d7b232b
Compare
Every other oper's exception set is a property of the node. A signed division's overflow was derived from the never-negative status of its operands, read from the local descriptor, the integral range and the value number store, so the set moved whenever a later phase learned something about an operand while GTF_EXCEPT stayed what morph computed. OperExceptions consults only the GTF_DIV_MOD_NO flags and constant operands. Whoever proves an operand safe records it on the node with SetDivModExceptionFlags: morph on every target, assertion propagation, and the stack level setter before codegen. Zero diffs on the osx-arm64 libraries pmi collection.
costEx prices a constant that fits a cmp or sub immediate the same as one that needs a mov, and one that fits an add immediate the same as one that takes movz and movk to reach a register. ConstantCost answers what the consuming instruction pays: nothing for an immediate it takes, otherwise the instructions that put the value in a register, with a ccmp's five bit immediate told apart from the twelve of cmp and sub. The range fold picks its encoding by it, and on arm64 the pair left alone, a cmp and ccmp chain with as many arithmetic instructions as the fold's sub and cmp, is one more candidate priced the same way. Compare chain formation declines a chain whose second constant fit its cmp but costs more than the removed branch to reach the ccmp.
NinoFloris
force-pushed
the
jit/modular-range-folds
branch
from
November 5, 2026 16:14
d7b232b to
f0b2d3c
Compare
AndyAyersMS
pushed a commit
that referenced
this pull request
Nov 7, 2026
Forming a chain moves the first block's condition into the second and compacts the two, and compaction gives the combined block the second block's weight. The second block's only predecessor is the first and the edge between them becomes unconditional, so the combined block runs as often as the first block and inherits its weight. This showed up as some false positive perf score regressions in #136994 because a fold keeps the first block's weight while the chain logic on main only kept the second. @AndyAyersMS
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to, and based on, #136926.