Skip to content

JIT: Remove redundant Span.Slice/Vector128.Create checks in vectorized loops - #135739

Merged
EgorBo merged 5 commits into
dotnet:mainfrom
EgorBo:opus-5-span-slice-bounds-checks
Oct 15, 2026
Merged

EgorBo merged 5 commits into
dotnet:mainfrom
EgorBo:opus-5-span-slice-bounds-checks

Conversation

@EgorBo

@EgorBo EgorBo commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Improve range-based relop folding so that the checks in typical vectorized span loops can be removed:

  • Fold "(uint)X > (uint)Bnd" (Span.Slice) via symbolic ranges relative to Bnd.
  • Fold "Bnd - X >= cns" (Vector128.Create length check) by computing the range of X relative to Bnd.
  • Use "Bnd1 == Bnd2" assertions to express limits of Bnd1 via the preferred Bnd2.
  • EvalRelop: compare "Bnd + cns" limits with the same Bnd.
  • Widen: retry monotonic analysis for negative constant lower bounds.

As the result, we fold all bound checks for:

static bool Equal(ReadOnlySpan<ushort> a, ReadOnlySpan<ushort> b)
{
    if (a.Length != b.Length)
        return false;

    int i = 0;
    if (Vector128.IsHardwareAccelerated)
    {
        for (; i <= a.Length - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
        {
            var va = Vector128.Create(a.Slice(i));
            var vb = Vector128.Create(b.Slice(i));
            if (va != vb)
                return false;
        }
    }

    for (; i < a.Length; i++)
    {
        if (a[i] != b[i])
            return false;
    }

    return true;
}

Before:

; Assembly listing for method Program:Test(System.ReadOnlySpan`1[ushort],System.ReadOnlySpan`1[ushort]):bool (FullOpts)
       push     r14
       push     rdi
       push     rsi
       push     rbp
       push     rbx
       sub      rsp, 32
       mov      rbx, bword ptr [rdx]
       mov      esi, dword ptr [rdx+0x08]
       mov      rdi, bword ptr [rcx]
       mov      ebp, dword ptr [rcx+0x08]
       cmp      ebp, esi
       jne      G_M43099_IG12
       xor      r14d, r14d
       lea      ecx, [rbp-0x08]
       test     ecx, ecx
       jl       SHORT G_M43099_IG06
       align    [0 bytes for IG04]
G_M43099_IG04:
       cmp      r14d, ebp
       ja       G_M43099_IG14
       mov      eax, r14d
       add      rax, rax
       lea      rdx, bword ptr [rdi+rax]
       mov      r8d, ebp
       sub      r8d, r14d
       cmp      r8d, 8
       jl       SHORT G_M43099_IG15
       vmovups  xmm0, xmmword ptr [rdx]
       cmp      r14d, esi
       ja       SHORT G_M43099_IG14
       add      rax, rbx
       mov      edx, esi
       sub      edx, r14d
       cmp      edx, 8
       jl       SHORT G_M43099_IG15
       vpcmpnequw k1, xmm0, xmmword ptr [rax]
       kortestb k1, k1
       jne      SHORT G_M43099_IG12
       add      r14d, 8
       cmp      r14d, ecx
       jle      SHORT G_M43099_IG04
G_M43099_IG06:
       cmp      r14d, ebp
       jge      SHORT G_M43099_IG10
       test     r14d, r14d
       jl       SHORT G_M43099_IG16
G_M43099_IG08:
       mov      eax, r14d
       movzx    rcx, word  ptr [rdi+2*rax]
       cmp      cx, word  ptr [rbx+2*rax]
       jne      SHORT G_M43099_IG12
       inc      r14d
       cmp      r14d, ebp
       jl       SHORT G_M43099_IG08
G_M43099_IG10:
       mov      eax, 1
       add      rsp, 32
       pop      rbx
       pop      rbp
       pop      rsi
       pop      rdi
       pop      r14
       ret
G_M43099_IG12:
       xor      eax, eax
       add      rsp, 32
       pop      rbx
       pop      rbp
       pop      rsi
       pop      rdi
       pop      r14
       ret
G_M43099_IG14:
       call     [System.ThrowHelper:ThrowArgumentOutOfRangeException()]
       int3
G_M43099_IG15:
       mov      ecx, 6
       call     [System.ThrowHelper:ThrowArgumentOutOfRangeException(int)]
       int3
G_M43099_IG16:
       cmp      r14d, ebp
       jae      SHORT G_M43099_IG18
       mov      eax, r14d
       movzx    rax, word  ptr [rdi+2*rax]
       cmp      r14d, esi
       jae      SHORT G_M43099_IG18
       mov      ecx, r14d
       cmp      ax, word  ptr [rbx+2*rcx]
       jne      SHORT G_M43099_IG12
       inc      r14d
       cmp      r14d, ebp
       jl       SHORT G_M43099_IG16
       jmp      SHORT G_M43099_IG10
G_M43099_IG18:
       call     CORINFO_HELP_RNGCHKFAIL
       int3
; Total bytes of code 236, prolog size 10, PerfScore 104.77, instruction count 87

After:

; Assembly listing for method Program:Test(System.ReadOnlySpan`1[ushort],System.ReadOnlySpan`1[ushort]):bool (FullOpts)
       push     rbx
       mov      rax, bword ptr [rdx]
       mov      edx, dword ptr [rdx+0x08]
       mov      r8, bword ptr [rcx]
       mov      ecx, dword ptr [rcx+0x08]
       cmp      ecx, edx
       jne      SHORT G_M43099_IG11
       xor      r10d, r10d
       lea      r9d, [rcx-0x08]
       test     r9d, r9d
       jl       SHORT G_M43099_IG06
       align    [3 bytes for IG04]
G_M43099_IG04:
       mov      r11d, r10d
       add      r11, r11
       lea      rbx, bword ptr [r8+r11]
       vmovups  xmm0, xmmword ptr [rbx]
       add      r11, rax
       vpcmpnequw k1, xmm0, xmmword ptr [r11]
       kortestb k1, k1
       jne      SHORT G_M43099_IG11
       add      r10d, 8
       cmp      r10d, r9d
       jle      SHORT G_M43099_IG04
G_M43099_IG06:
       cmp      r10d, ecx
       jge      SHORT G_M43099_IG09
G_M43099_IG07:
       mov      edx, r10d
       movzx    r9, word  ptr [r8+2*rdx]
       cmp      r9w, word  ptr [rax+2*rdx]
       jne      SHORT G_M43099_IG11
       inc      r10d
       cmp      r10d, ecx
       jl       SHORT G_M43099_IG07
G_M43099_IG09:
       mov      eax, 1
       pop      rbx
       ret
G_M43099_IG11:
       xor      eax, eax
       pop      rbx
       ret
; Total bytes of code 110, prolog size 1, PerfScore 75.24, instruction count 38

Quite big Diffs

…d loops

Improve range-based relop folding so that the checks in typical vectorized
span loops can be removed:

* Fold "(uint)X > (uint)Bnd" (Span.Slice) via symbolic ranges relative to Bnd.
* Fold "Bnd - X >= cns" (Vector128.Create length check) by computing the range
  of X relative to Bnd.
* Use "Bnd1 == Bnd2" assertions to express limits of Bnd1 via the preferred Bnd2.
* EvalRelop: compare "Bnd + cns" limits with the same Bnd.
* Widen: retry monotonic analysis for negative constant lower bounds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 23323467-e54f-4591-bf7e-c52042612588
@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 Oct 14, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new range derivation can negate INT32_MIN and lacks targeted codegen regression coverage.

2 open findings
What changed in this PR

Enhances JIT range analysis to eliminate redundant checks in vectorized span loops.

Changes:

  • Compares symbolic limits sharing a checked bound.
  • Derives ranges for Bnd - X and equivalent bounds.
  • Improves loop widening for negative lower bounds.
File Description
rangecheck.h Extends symbolic relational folding.
rangecheck.cpp Improves widening and equal-bound handling.
compiler.h Declares new range-analysis helpers.
assertionprop.cpp Adds bound selection and subtraction-range analysis.

🧠 Review effort: Balanced

Comment thread src/coreclr/jit/assertionprop.cpp Resolved
Comment thread src/coreclr/jit/assertionprop.cpp Resolved
Widen now also tightens negative constant lower limits via monotonicity, but
DoesVarDefOverflow skips the def-chain overflow check for non-dependent limits,
so a wrapping IV (e.g. i += int.MaxValue) could get a bogus non-negative lower
bound. Re-run the overflow check with a dependent lower limit first.

Also avoid negating INT32_MIN in GetBoundMinusXRange.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2c55bab0-dbf9-4ac2-aa19-38b89f405ecc
@EgorBo
EgorBo marked this pull request as ready for review October 14, 2026 11:56
@EgorBo

EgorBo commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

PTAL @tannergooding @dotnet/jit-contrib

I did some fuzzy testing + AI validation and found no issues. Since it's early in the cycle, I think it's safe to merge.
Quite a few diffs + it allows to replace a few unsafe SIMD algorthims with safe now. Previously, only while (span.Length >= CNS) shape (for single span) worked well with that.

@tannergooding

Copy link
Copy Markdown
Member

@EgorBo I think the practical is we have up to 4 spans involved, will this work for that too?

In particular we have cases like TensorPrimitives.FusedMultiplyAdd which is:

  • ROSpan<T> left
  • ROSpan<T> right
  • ROSpan<T> addend
  • Span<T> destination

and the current guard we do is:

if (left.Length != right.Length || left.Length != addend.Length)
{
    ThrowHelper.ThrowArgument_SpansMustHaveSameLength();
}

if (left.Length > destination.Length)
{
    ThrowHelper.ThrowArgument_DestinationTooShort();
}

So we essentially ensure:

  1. All inputs are the same length
  2. The destination is at least as big as the inputs

We could do something like destination = destination[..left.Length] and explicitly slice it to the same length as the inputs if that helps; but I imagine it shouldn't be required.


The loop we're doing is then essentially (if we converted it from unsafe to span):

while (left.Length > (Vector<T>.Count * UnrollFactor))
{
    tmp1 = Operator(Vector.Create(left[(Vector<T>.Count * 0)..]), Vector.Create(right[(Vector<T>.Count * 0)..]), Vector.Create(addend[(Vector<T>.Count * 0)..]));
    tmp2 = Operator(Vector.Create(left[(Vector<T>.Count * 1)..]), Vector.Create(right[(Vector<T>.Count * 1)..]), Vector.Create(addend[(Vector<T>.Count * 1)..]));
    // ... UnrollFactor times

    tmp1.CopyTo(destination[(Vector<T>.Count * 0)..]);
    tmp2.CopyTo(destination[(Vector<T>.Count * 1)..]);
    // ... UnrollFactor times

    left = left[(Vector<T>.Count * UnrollFactor)..];
    right = right[(Vector<T>.Count * UnrollFactor)..];
    addend = addend[(Vector<T>.Count * UnrollFactor)..];
    destination = destination[(Vector<T>.Count * UnrollFactor)..];
}

So the ideal from the span work is this scenario works, so we're covering multiple spans and unrolling within the same loop.

The efficient trailing element handling is then the other relevant part, where we have some switch jump table, but I imagine that is something to look at separately...

Comment thread src/coreclr/jit/assertionprop.cpp Resolved
Comment thread src/coreclr/jit/rangecheck.cpp Resolved

@tannergooding tannergooding left a comment

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.

Changes LGTM. There's one small ordering pessimization (I think we should fix this) and potentially a missed scenario (I don't think this is super relevant)

I also gave an example of the "worst case" I know we actually want to support, which I think this will handle but it might be good to validate.

Check the "Bnd == PreferredBnd" case first, so the limit is expressed via the preferred bound even when Bnd is never negative (e.g. lengths of spans created from arrays).

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

Copilot-Session: 2942b3d9-86e0-4bf3-8e10-5e401a447573
Replace AreVNsEqual with a short chain search over "==", "<" and "<=" assertions, so "X <= Bnd + cns" can be expressed via the preferred bound when "Bnd == PreferredBnd" holds transitively (e.g. "a == b && b == c"), or when "Bnd <= PreferredBnd" holds for an upper limit with cns <= 0 and a never negative Bnd.

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

Copilot-Session: 2942b3d9-86e0-4bf3-8e10-5e401a447573
Follow SSA copies through gtEffectiveVal so "Bnd - X" is found behind CSE uses ("tmp = cse") and defs ("tmp = COMMA(STORE(cse, Bnd - X), cse)").

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

Copilot-Session: 2942b3d9-86e0-4bf3-8e10-5e401a447573
@EgorBo

EgorBo commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@tannergooding thanks, addressed your feedback, it increased the diffs even more (up to -840k)

Unfortunately, we still can't handle the shape used in TensorPrimitives (but this PR makes a step towards that).

@EgorBo
EgorBo merged commit 11ce222 into dotnet:main Oct 15, 2026
127 of 129 checks passed
@EgorBo
EgorBo deleted the opus-5-span-slice-bounds-checks branch October 15, 2026 17:40
@tannergooding

Copy link
Copy Markdown
Member

nice! and sounds good on TensorPrimitives. We're getting closer at least

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 reduce-unsafe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants