Skip to content

JIT: fold relop on ADD(base, 1) using base-vs-other assertion#128101

Draft
EgorBo wants to merge 6 commits into
mainfrom
jit-fold-add-by-one
Draft

JIT: fold relop on ADD(base, 1) using base-vs-other assertion#128101
EgorBo wants to merge 6 commits into
mainfrom
jit-fold-add-by-one

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented May 12, 2026

Fixes #112953

Allows the JIT to remove the redundant Slice bounds check in patterns like:

ReadOnlySpan<char> Test(ReadOnlySpan<char> span, int offset)
{
    if ((uint)offset < (uint)span.Length)
    {
        return span.Slice(offset + 1);
    }
    return span;
}
 G_M56038_IG02:
        mov      rsi, bword ptr [rdx]
        mov      edi, dword ptr [rdx+0x08]
        cmp      r8d, edi
        jae      SHORT G_M56038_IG08
 G_M56038_IG03:
        inc      r8d
-       cmp      r8d, edi
-       ja       SHORT G_M56038_IG09
        mov      ecx, r8d
        lea      rsi, bword ptr [rsi+2*rcx]
        sub      edi, r8d
        jns      SHORT G_M56038_IG05
-G_M56038_IG09:
-       call     [System.ThrowHelper:ThrowArgumentOutOfRangeException()]
-       int3

Copilot AI review requested due to automatic review settings May 12, 2026 21:20
@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 May 12, 2026
@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.

@EgorBo EgorBo force-pushed the jit-fold-add-by-one branch from 1aae4b7 to 812bcc3 Compare May 12, 2026 21:25
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 enhances CoreCLR JIT range-check elimination by using existing relational assertions to fold comparisons involving ADD(base, 1) (e.g., enabling removal of redundant ReadOnlySpan<T>.Slice(offset + 1) bounds checks when guarded by (uint)offset < (uint)len).

Changes:

  • Add a new JIT regression test covering the Slice(offset + 1) pattern and asserting no remaining range-check-fail helper in the generated code.
  • Extend RangeCheck::GetRangeFromAssertionsWorker to fold certain ADD(base, 1) relops to constant true/false when a matching strict < assertion exists (with matching signedness).
  • Refine unsigned assertion/range handling: allow some unsigned VN-vs-VN assertion tightening in MergeEdgeAssertionsWorker, and reduce assertion-table pressure by skipping complementary assertions for some unsigned cases.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs Adds a regression test for eliding the redundant Slice(offset + 1) bounds check under a (uint)offset < (uint)Length guard.
src/coreclr/jit/rangecheck.cpp Adds assertion-driven folding for ADD(base, 1) relops and improves unsigned VN-assertion range tightening rules.
src/coreclr/jit/assertionprop.cpp Expands VN-relop assertion creation to include unsigned int32 relops (with existing pressure gating) and avoids generating low-value unsigned complementary assertions.

Comment thread src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs
Copilot AI review requested due to automatic review settings May 12, 2026 21:28
@github-actions

This comment has been minimized.

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/jit/rangecheck.cpp Outdated
EgorBo and others added 2 commits May 12, 2026 23:50
Allows global assertion prop to remove the redundant Slice-style bounds
check in patterns like:

    if ((uint)offset < (uint)span.Length)
        return span.Slice(offset + 1);

Changes in src/coreclr/jit/:

* optCreateJTrueBoundsAssertion: also create O2K_VN relop assertions for
  unsigned compares (still gated by optAssertionHasAssertionsForVN to
  cap table pressure).

* optCreateComplementaryAssertion: skip the complementary for unsigned
  LT_UN/LE_UN with O2K_VN op2, by analogy with the existing skip for
  O2K_CHECKED_BOUND_ADD_CNS. Unsigned '>'/'>=' complementaries describe
  non-contiguous regions in the signed-int domain that Range can't
  represent and rarely match a direct relop fold.

* MergeEdgeAssertionsWorker: derive a constant range from unsigned LT/LE
  O2K_VN assertions when the other operand has a non-negative range;
  unsigned GT/GE remain skipped (non-contiguous range).

* GetRangeFromAssertionsWorker: when EvalRelop is inconclusive, fold
  '<cmp>(ADD(base, 1), other)' using an asserted 'base < other' with
  matching signedness. K is restricted to 1: the assertion itself rules
  out base==INTxxx_MAX, so base+1 cannot overflow in either signedness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@EgorBo EgorBo force-pushed the jit-fold-add-by-one branch from e673c2b to 8606c7c Compare May 12, 2026 21:51
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings May 12, 2026 23:21
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

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings May 13, 2026 01:56
@EgorBo EgorBo force-pushed the jit-fold-add-by-one branch from 4a8e166 to 27352f6 Compare May 13, 2026 01:56
@github-actions
Copy link
Copy Markdown
Contributor

Caution

Security scanning requires review for Code Review

Details

The threat detection results could not be parsed. The workflow output should be reviewed before merging.

Review the workflow run logs for details.

Note

This review was generated by Copilot.

🤖 Copilot Code Review — PR #128101

Holistic Assessment

Motivation: Justified. The linked issue (#112953) demonstrates a real missed optimization in a common Span.Slice(offset + 1) pattern after a manual bounds check. This affects real-world IndexOf+Slice loops.

Approach: Sound. The PR extends the existing assertion propagation and range-check infrastructure in two complementary ways: (1) creating unsigned relop VN assertions so the JIT records (uint)offset < (uint)len, and (2) adding a targeted fold in RangeCheck that proves ADD(base, 1) <= other from an asserted base < other. The mathematical proof is correct and well-documented.

Summary: ✅ LGTM. The changes are well-scoped, the correctness argument is rigorous, edge cases are properly guarded, and the test validates the motivating pattern. Minor observations below.


Detailed Findings

✅ Correctness of TryFoldRelopOfAddByOneFromAssertions

The core proof is correct for both signed and unsigned domains:

  • Unsigned: base u< otherbase ≤ UINT32_MAX − 1base + 1 cannot wrap ⟹ base + 1 u≤ other.
  • Signed: base < otherbase ≤ INT32_MAX − 1 (since other ≤ INT32_MAX) ⟹ no signed overflow.

The signedness-matching guard (aIsUnsigned != isUnsigned) is essential and correctly prevents cross-domain reasoning. The base == op2VN guard correctly rejects the degenerate ADD(x, 1) vs x case. The restriction to addCns == 1 is conservative but sufficient for the motivating pattern.

✅ Unsigned assertions in MergeEdgeAssertionsWorker

The new unsigned LT/LE handling in MergeEdgeAssertionsWorker is well-guarded:

  • Only GT_LT/GT_LE directions are accepted (GT/GE would produce non-contiguous ranges in the signed domain).
  • The otherRange.LowerLimit().GetConstant() < 0 check ensures the derived range stays in the non-negative signed domain, which the Range representation requires.

✅ Complementary assertion suppression

Extending the complementary-skip to O2K_VN alongside O2K_CHECKED_BOUND_ADD_CNS for OAK_LT_UN/OAK_LE_UN is correct. Unsigned > / >= complementaries can't tighten signed-int ranges and would waste assertion table slots.

✅ Test coverage

The SliceOffsetPlusOne test covers the core optimization (offset in-range) and the fallthrough (offset out-of-range). The disassembly check (X64-NOT: ThrowArgumentOutOfRangeException) validates that the bounds check is actually elided.

💡 Boundary edge case test

The test doesn't exercise offset == span.Length - 1 (e.g., SliceOffsetPlusOne("hello".AsSpan(), 4) → expected length 0), which is the tightest boundary where offset + 1 == span.Length. This was noted in a prior review comment (now resolved). Not blocking since the proof covers this case, but it would strengthen confidence.

💡 Generalization beyond +1

The fold is hardcoded to addCns == 1. A future generalization to arbitrary positive constants (base + K <= other from base < other when K >= 1 and other >= K) could help patterns like Slice(offset + 2). Not in scope for this PR.

Generated by Code Review for issue #128101 · ● 2.3M ·

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines 1830 to 1842
// "X relop Y" where neither side is a constant nor a checked bound.
// For now, we only create such assertions for signed comparisons of int32 (and smaller, after promotion).
// We create such assertions for both signed and unsigned comparisons of int32 (and smaller, after promotion).
// This widens what global assertion prop can reason about: e.g. "b > a" combined with "a > 10"
// can be used to deduce "b > 10".
// can be used to deduce "b > 10". The unsigned variant enables folding patterns like
// "(uint)(i + 1) <= (uint)len" given "(uint)i < (uint)len".
//
// To keep table pressure under control, we only create the assertion if at least one of the
// operands already has assertions registered. Otherwise the new assertion has no other facts
// it can chain with and is unlikely to enable any deduction, while still consuming a slot
// (and potentially crowding out useful ones).
if (!isUnsignedRelop && (op1VN != op2VN) && !vnStore->IsVNConstant(op1VN) && !vnStore->IsVNConstant(op2VN) &&
if ((op1VN != op2VN) && !vnStore->IsVNConstant(op1VN) && !vnStore->IsVNConstant(op2VN) &&
(optAssertionHasAssertionsForVN(op1VN) || optAssertionHasAssertionsForVN(op2VN)))
{
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.

JIT can't elide span.Slice range checks after a manual check

2 participants