Uh oh!
There was an error while loading. Please reload this page.
JIT: eliminate bounds checks for "i != arr.Length" loops - #129199
JIT: eliminate bounds checks for "i != arr.Length" loops#129199AndyAyersMS wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Updates CoreCLR JIT loop analysis, loop cloning, assertion propagation, and range analysis to enable bounds-check elimination for i != end loop forms (with strict stride constraints), aligning them more closely with equivalent < / > loops and improving both optimization and soundness.
Changes:
- Teach
NaturalLoopIterInfoto classifyGT_NEloop tests as increasing/decreasing only when the IV step is exactly+1/-1. - Extend loop cloning (
optDeriveLoopCloningConditions) to acceptGT_NEtests and add the necessary entry/zero-trip and per-access guards to keep the fast clone sound. - Extend assertion creation + RangeCheck merging so
==/!=assertions against checked bounds can tighten IV ranges and enable BCE even when loop cloning can’t apply. - Add a JIT regression test covering both the optimization shape and key soundness cases.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tests/JIT/Regression/JitBlue/Runtime_84697/Runtime_84697.cs | Adds regression coverage for i != Length loops across arrays/strings/spans and includes soundness checks for misordered init and decreasing-loop two-array scenarios. |
| src/coreclr/jit/flowgraph.cpp | Extends increasing/decreasing loop classification to recognize GT_NE only for stride ±1 to avoid non-terminating/wrapping cases. |
| src/coreclr/jit/loopcloning.cpp | Enables loop cloning reasoning for GT_NE, adds ordered entry guards and an init-vs-limit guard to prevent wraparound on the fast path, and derives per-access conditions for GT_NE. |
| src/coreclr/jit/assertionprop.cpp | Allows optCreateJTrueBoundsAssertion to create checked-bound equality assertions (EQ/NE) where useful for RangeCheck while preserving existing constant-equality assertion behavior. |
| src/coreclr/jit/rangecheck.cpp | Teaches RangeCheck to tighten ranges from X == bound / X != bound assertions where bound is a checked bound, enabling BCE in non-cloneable cases. |
| // GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop): | ||
| // For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT) | ||
| // For decreasing: visited indices are [end+1..init] => guard end < arrLen (same as GT) | ||
| // |
Loops with `i != end` previously kept their per-iteration bounds checks while the equivalent `<` / `>` forms did not: loop cloning bailed on GT_NE, and RangeCheck couldn't refine the IV's range from a "back-edge != end" assertion. Recognize GT_NE with stride exactly +/-1 as an increasing or decreasing loop in NaturalLoopIterInfo, extend optDeriveLoopCloningConditions to emit the right per-access and zero-trip conditions for GT_NE, and add a new "init RELOP end" cloning condition so that a misordered init (which for GT_NE would wrap the IV through the type rather than exit at the first test) falls back to the slow path. Builds on dotnet#129176 which fixes the latent decreasing-loop soundness gap. Also extend optCreateJTrueBoundsAssertion to create CompareCheckedBound assertions for EQ/NE against a checked bound, and teach RangeCheck::MergeAssertion to tighten ranges using `X != bound` / `X == bound`. This catches NE-loop BCE in cases that fall outside loop cloning (e.g. loops with side-effecting calls in the body). For equality assertions where the non-bound side is a constant, defer to the existing LCLVAR-based equality assertion path to preserve downstream consumers that rely on it (e.g. proving "len > 0" after a "len != 0" check). Codegen for `for (int i = 0; i != src.Length; i++) sum += src[i]` drops from 64 to 30 bytes. Fixesdotnet#84697. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply jit-format fixes from CI and clarify the GT_NE comment in optDeriveLoopCloningConditions to call out that the decreasing-loop soundness relies on `ident` being the init value (per Copilot bot review of dotnet#129199).
a9bfb4a to
834ca4aCompare| // [lo, bound] -> [lo, bound - 1] | ||
| // [bound, hi] -> [bound + 1, hi] | ||
| if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) && | ||
| (pRange->UpperLimit().GetConstant() == 0)) |
There was a problem hiding this comment.
presumably we don't have to check ah, actually nvm, it's not correctpRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
AndyAyersMS
commented
Jun 11, 2026
Going to split this in two.. |
Loops with
i != endpreviously kept their per-iteration bounds checks while the equivalent</>forms did not: loop cloning bailed on GT_NE, and RangeCheck couldn't refine the IV's range from a "back-edge != end" assertion.Recognize GT_NE with stride exactly +/-1 as an increasing or decreasing loop in NaturalLoopIterInfo, extend optDeriveLoopCloningConditions to emit the right per-access and zero-trip conditions for GT_NE, and add a new "init RELOP end" cloning condition so that a misordered init (which for GT_NE would wrap the IV through the type rather than exit at the first test) falls back to the slow path. Builds on #129176 which fixes the latent decreasing-loop soundness gap.
Also extend optCreateJTrueBoundsAssertion to create CompareCheckedBound assertions for EQ/NE against a checked bound, and teach RangeCheck::MergeAssertion to tighten ranges using
X != bound/X == bound. This catches NE-loop BCE in cases that fall outside loop cloning (e.g. loops with side-effecting calls in the body). For equality assertions where the non-bound side is a constant, defer to the existing LCLVAR-based equality assertion path to preserve downstream consumers that rely on it (e.g. proving "len > 0" after a "len != 0" check).Codegen for
for (int i = 0; i != src.Length; i++) sum += src[i]drops from 64 to 30 bytes.Fixes#84697.