JIT: coalesce constant-indexed bounds checks within a block#127439
JIT: coalesce constant-indexed bounds checks within a block#127439AndyAyersMS wants to merge 1 commit intodotnet:mainfrom
Conversation
Add a new phase `optBoundsCheckCoalesce` that runs before assertion prop, looking for sequences of bounds checks that can be collapsed into a single dominating check. For example: `a[0] + a[1] + a[2] + a[3]` produces four bounds checks with indices 0, 1, 2, 3 and the same length VN. The phase rewrites the first check index to 3 and marks the other three checks as "in bound" so they get removed during assertion prop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@EgorBo FYI |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR adds a new JIT optimization phase (optBoundsCheckCoalesce) that runs before assertion propagation to reduce redundant bounds checks for constant indices within a basic block by strengthening one dominating check and marking others as provably in-bounds for later removal.
Changes:
- Introduces a new JIT phase (
PHASE_BOUNDS_CHECK_COALESCE) and schedules it before assertion propagation. - Adds
Compiler::optBoundsCheckCoalesce()and implements block-local coalescing for constant-index bounds checks with the same length VN. - Wires the new implementation into the JIT build (CMake sources).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/compphases.h | Registers the new phase name/id for diagnostics and phase tracking. |
| src/coreclr/jit/compiler.h | Declares optBoundsCheckCoalesce() on Compiler. |
| src/coreclr/jit/compiler.cpp | Runs the new coalescing phase immediately before assertion propagation. |
| src/coreclr/jit/boundscheckcoalesce.cpp | Implements the new bounds-check coalescing pass. |
| src/coreclr/jit/CMakeLists.txt | Adds the new .cpp file to the JIT build sources. |
| bool IsSideEffectBarrier(Compiler* comp, GenTree* node, bool blockIsInsideTry) | ||
| { | ||
| if (node->IsCall()) | ||
| { | ||
| return true; | ||
| } | ||
| if (node->OperIs(GT_MEMORYBARRIER)) |
There was a problem hiding this comment.
IsSideEffectBarrier only treats calls/stores/atomics/memory barriers as blockers, but the transformation can also change observable exception ordering across other potentially-throwing or ordering-constrained nodes (e.g., checked overflow ops, div/mod, nullchecks/indirections on unrelated objects, or nodes with GTF_ORDER_SIDEEFF). Strengthening an earlier bounds check to a larger index can make IndexOutOfRangeException occur before an intervening DivideByZeroException/OverflowException/NullReferenceException that would have been thrown first per the original evaluation order. Consider extending the barrier criteria to include nodes that may throw (or at least those with exception sets beyond the bounds-check group) and nodes with GTF_ORDER_SIDEEFF, so coalescing only happens when it cannot change which exception is observed.
There was a problem hiding this comment.
It does seem kind of ad hoc, can't we just use exception flags here?
| // Follower: tag for forward assertion prop to splice out, and | ||
| // bump the head's running max offset. | ||
| BoundsCheckCandidate& head = candidates.BottomRef(headIndex); | ||
| JITDUMP("BC coalesce in " FMT_BB ": marking [%06u] (offset %d) as redundant of [%06u]\n", block->bbNum, | ||
| dspTreeID(bc), offset, dspTreeID(head.m_bc)); | ||
| bc->gtFlags |= GTF_CHK_INDEX_INBND; | ||
| if (offset > head.m_offset) |
There was a problem hiding this comment.
This phase mutates IR by setting GTF_CHK_INDEX_INBND on follower bounds checks, but modified is only set when a head check is strengthened. If no head needs strengthening (because the first check already has the max index), PhaseStatus will report MODIFIED_NOTHING even though flags were changed, which can make phase accounting/debugging misleading. Track whether any follower was marked redundant and reflect that in the returned PhaseStatus.
| // Coalesce groups of constant-indexed bounds checks. | ||
| // | ||
| DoPhase(this, PHASE_BOUNDS_CHECK_COALESCE, &Compiler::optBoundsCheckCoalesce); |
There was a problem hiding this comment.
The new optBoundsCheckCoalesce phase introduces non-trivial new optimization behavior and safety constraints; there should be a focused JIT regression test that verifies (1) multiple constant-index bounds checks within a block coalesce to a single check (e.g., by asserting fewer/no CORINFO_HELP_RNGCHKFAIL occurrences in disasm) and (2) semantics are preserved for nearby exception-throwing operations. Consider adding coverage under src/tests/JIT/opt/RangeChecks alongside existing bounds-check tests.
|
should it already be optimized today via block clonning? static int Max(int[] a)
{
return a[0] + a[1] + a[2] + a[3];
}; Method MinMaxBench:Max(int[]):int (FullOpts)
G_M60565_IG01: ;; offset=0x0000
sub rsp, 40
G_M60565_IG02: ;; offset=0x0004
mov edx, dword ptr [rcx+0x08]
cmp edx, 3
jle SHORT G_M60565_IG04
mov eax, dword ptr [rcx+0x10]
add eax, dword ptr [rcx+0x14]
add eax, dword ptr [rcx+0x18]
add eax, dword ptr [rcx+0x1C]
G_M60565_IG03: ;; offset=0x0018
add rsp, 40
ret
G_M60565_IG04: ;; offset=0x001D
test edx, edx
je SHORT G_M60565_IG06
mov eax, dword ptr [rcx+0x10]
cmp edx, 1
jbe SHORT G_M60565_IG06
add eax, dword ptr [rcx+0x14]
cmp edx, 2
jbe SHORT G_M60565_IG06
add eax, dword ptr [rcx+0x18]
cmp edx, 3
jbe SHORT G_M60565_IG06
add eax, dword ptr [rcx+0x1C]
G_M60565_IG05: ;; offset=0x003C
add rsp, 40
ret
G_M60565_IG06: ;; offset=0x0041
call CORINFO_HELP_RNGCHKFAIL
int3
; Total bytes of code: 71I assume in this specific case we can avoid clonning, but this will be just about removing a cold block? |
|
It does trim away some of the cloning cold code, but also seems to get other cases. Will post some examples in a bit. diffs -- lots of hits in tests, less in more realistic stuff. A case where this is not just streamlining range cloning (there are more, just pointing one out that is readily visible from the diffs) aspnet2
|
| candidates.Reset(); | ||
| groupMap.RemoveAll(); | ||
| int barrierCount = 0; | ||
| bool const blockIsInsideTry = block->hasTryIndex(); |
There was a problem hiding this comment.
I think this should use BasicBlock::HasPotentialEHSuccs to be correct.

Add a new phase
optBoundsCheckCoalescethat runs before assertion prop, looking for sequences of bounds checks that can be collapsed into a single dominating check.For example:
a[0] + a[1] + a[2] + a[3]produces four bounds checks with indices 0, 1, 2, 3 and the same length VN. The phase rewrites the first check index to 3 and marks the other three checks as "in bound" so they get removed during assertion prop.