JIT: Elide BOUNDS_CHECK(0, length) when length != 0 is proven by assertion#126856
JIT: Elide BOUNDS_CHECK(0, length) when length != 0 is proven by assertion#126856
Conversation
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/81448e73-1d65-4ae9-90d4-38de0a5459de Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/81448e73-1d65-4ae9-90d4-38de0a5459de Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR improves CoreCLR JIT range-check elimination by recognizing that a dominating length != 0 assertion is sufficient to prove BOUNDS_CHECK(0, length) cannot throw, enabling removal of redundant bounds checks in common span/array guard patterns.
Changes:
- Update
optAssertionProp_BndsChkto treat a livelenVN != 0assertion as implyinglenLo >= 1(given non-negative length), allowingindex==0bounds checks to be elided. - Add a JIT regression test covering the canonical
if (span.IsEmpty) return ...; return span[0];pattern and assert noCORINFO_HELP_RNGCHKFAILappears in x64/arm64 asm.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/coreclr/jit/assertionprop.cpp | Tightens inferred length lower bound to enable dropping BOUNDS_CHECK(0, len) when len != 0 is asserted. |
| src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs | Adds a regression case validating bounds-check elision after an IsEmpty guard. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/4739c73a-b933-4754-a5c7-063f961e69b0 Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Updated the PR description to remove ARM64-specific language and added |
|
PTAL @dotnet/jit-contrib trivial change with diffs |
BOUNDS_CHECK(0, length)checks(uint)0 < (uint)length, which is semantically equivalent tolength != 0. However, the JIT failed to eliminate this check even when a dominatinglength != 0assertion was live — a common pattern after null/empty guards on spans and arrays.Root cause
optAssertionProp_BndsChkcomputes constant ranges viaGetRangeFromAssertions. That function'sOAK_NOT_EQUALhandler can only tighten a range when the excluded constant matches the current lower or upper bound. Starting from[INT32_MIN, INT32_MAX], alength != 0assertion never narrows the range, solenLostays atINT32_MINand theidxHi < lenLocheck always fails.Fix
src/coreclr/jit/assertionprop.cpp— AfterGetRangeFromAssertions, when the index is known to be exactly 0 andlenLo <= 0, scan live assertions for anOAK_NOT_EQUALon the length VN against constant0. If found, tightenlenLoto1.GT_BOUNDS_CHECKguarantees the length operand is non-negative, solength != 0⟹length >= 1, makingidxHi=0 < lenLo=1hold and the check drops.src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs— AddTryStripFirstChardisasm check test case (repro from Bounds check around Span.IsEmpty is not elided #126857) verifying the bounds check is eliminated for the canonical pattern:Generated asm confirms no
CORINFO_HELP_RNGCHKFAILcall — just a zero-check on the length followed by a direct element access.Original prompt
[Chronological Review] 1. User reported redundant `tbnz` instructions on ARM64 in profiler codegen, asked for emitter-level peephole fix 2. Explored emitter infrastructure: `emitLastIns`, `emitCanPeepholeLastIns()`, existing peepholes like `IsRedundantMov`, `IsRedundantLdStr`, `OptimizeLdrStr` 3. Found emission functions: `emitIns_J_R` (cbz/cbnz), `emitIns_J_R_I` (tbz/tbnz), `emitIns_J` (b.cond) 4. Implemented emitter peephole `IsRedundantBranch` in emitarm64.cpp/h - build clean, SPMI clean (14707 contexts) 5. User asked to revert emitter changes and investigate SPMI diff file to understand WHY the redundant instruction appears 6. Analyzed diff_summary.6.md - found clear pattern: `cbz wN, IG_normalPath` followed by redundant `cbz wN, IG_throwHelper` 7. Investigated `optRedundantBranch` in redundantbranchopts.cpp - only handles GT_JTRUE, not GT_JCMP/GT_JTEST 8. Discovered root cause: GT_BOUNDS_CHECK stays as a node through all optimizer phases, only expanded to cbz during codegen 9. Analyzed assertionprop.cpp and rangecheck.cpp to understand why bounds check isn't eliminated 10. User asked to bring back emitter changes minimally - started implementing 11. User changed mind: asked to implement assertion-level fix instead (options 1 and 2) 12. Implemented fix in assertionprop.cpp (optAssertionProp_BndsChk) and rangecheck.cpp (MergeEdgeAssertions with IsVNCheckedBound) 13. User correctly pointed out: IsVNCheckedBound alone doesn't prove non-negativity - the rangecheck.cpp change is invalid 14. Reverted both changes, reconsidered the problem 15. Key insight: BOUNDS_CHECK(0, length) checks `(uint)0 < (uint)length` ≡ `length != 0`. So `length != 0` assertion directly proves bounds check won't throw, regardless of sign 16. Implemented correct fix: single change in assertionprop.cpp scanning for OAK_NOT_EQUAL assertion on length VN with constant 0 17. Build clean, SPMI x64 clean (14707 contexts)[Intent Mapping]
[Technical Inventory]
[Code Archaeology]
[Progress Assessment]
[Context Validation]
All critical context captured: the diff, the reasoning, the incorrect attempt, and the correct fix.
[Recent Commands Analysis]
Most recent operations:
git checkout -- src/coreclr/jit/rangecheck.cpp src/coreclr/jit/assertionprop.cpp- reverted invalid changesediton assertionprop.cpp - added new bounds check elimination for index==0 with length!=0 assertionbuild.cmd Clr.AllJits -c Checked- built successfully (0 warnings, 0 errors)git diff- showed final single-file change1. Conversation Overview: - Primary Objectives: User initially asked to remove redundant conditional branch instructions (tbnz/tbz/cbz/cbnz) on ARM64 using emitter peephole. After investigation, pivoted to fixing the root cause in assertion propagation to eliminate redundant bounds checks that produce the duplicate branches. - Session Context: Started with emitter-level fix → investigated SPMI diffs to find root cause → discovered bounds checks survive through optimizer a...
Created from Copilot CLI via the copilot delegate command.