Skip to content

JIT: coalesce constant-indexed bounds checks within a block#127439

Draft
AndyAyersMS wants to merge 1 commit intodotnet:mainfrom
AndyAyersMS:bc-coalesce
Draft

JIT: coalesce constant-indexed bounds checks within a block#127439
AndyAyersMS wants to merge 1 commit intodotnet:mainfrom
AndyAyersMS:bc-coalesce

Conversation

@AndyAyersMS
Copy link
Copy Markdown
Member

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.

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>
Copilot AI review requested due to automatic review settings April 26, 2026 22:28
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Apr 26, 2026
@AndyAyersMS
Copy link
Copy Markdown
Member Author

@EgorBo FYI

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +77 to +83
bool IsSideEffectBarrier(Compiler* comp, GenTree* node, bool blockIsInsideTry)
{
if (node->IsCall())
{
return true;
}
if (node->OperIs(GT_MEMORYBARRIER))
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem kind of ad hoc, can't we just use exception flags here?

Comment on lines +200 to +206
// 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)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +4810 to +4812
// Coalesce groups of constant-indexed bounds checks.
//
DoPhase(this, PHASE_BOUNDS_CHECK_COALESCE, &Compiler::optBoundsCheckCoalesce);
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@EgorBo
Copy link
Copy Markdown
Member

EgorBo commented Apr 26, 2026

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: 71

I assume in this specific case we can avoid clonning, but this will be just about removing a cold block?

@AndyAyersMS
Copy link
Copy Markdown
Member Author

AndyAyersMS commented Apr 26, 2026

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

image

candidates.Reset();
groupMap.RemoveAll();
int barrierCount = 0;
bool const blockIsInsideTry = block->hasTryIndex();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should use BasicBlock::HasPotentialEHSuccs to be correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants