JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

JIT: extend loop cloning for span+stride>1 and ±const limits - #129309

Merged
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv
Jun 12, 2026
Merged

JIT: extend loop cloning for span+stride>1 and ±const limits#129309
AndyAyersMS merged 4 commits into
dotnet:mainfrom
AndyAyersMS:loop-clone-non-unit-iv

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
Member

Enable cloning of loops over Span when the stride s is greater than 1, guarded by a runtime limit <= INT_MAX - s + 1 condition on the increasing var-limit case. Other forms are already implicitly safe.

Extend MatchLimit to peel a constant offset off the limit so loops like

for(inti=0;i<span.Length-K;i+=K){ ...}

become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a new arr.Length + offset >= 0 guard (for very short array lengths and negative offsets).

Enable cloning of loops over Span<T> when the stride is greater than 1,
guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case (other forms are already implicitly safe).
Extend MatchLimit to peel a constant offset off the limit so loops like
`for (i; i < arr.Length - K; i += K)` -- the common Vector<T>.Count
vectorization warm-up -- become clonable. NaturalLoopIterInfo gains a
LimitOffset that flows through LC_Ident (Var and ArrAccess gain an
offset), the zero-trip / per-access / NE / overflow guards, and a new
`arr.Length + offset >= 0` guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 11, 2026 20:35
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 11, 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.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

@EgorBo PTAL
fyi @dotnet/jit-contrib

Impacts ~200 methods across SPMI. If you know of specific cases that should be handled in BCL / etc let me know and I'll verify.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Also want to see if IV opts can now understand the IV does not overflow in the fast path.

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

Pull request overview

This PR extends CoreCLR JIT loop cloning’s ability to reason about loop limits and spans by (1) modeling limitBase ± const limits as a base + offset and (2) enabling span-based loop cloning for non-unit strides with an added overflow safety guard. It also adds new JIT tests that exercise non-unit stride span loops and offset limits like Length - K.

Changes:

  • Add NaturalLoopIterInfo::LimitOffset plus LimitBase() to represent and consume base ± const loop limits.
  • Extend LC_Ident to carry an offset for Var and ArrAccess, and plumb it through condition generation.
  • Update loop cloning condition derivation to support span stride > 1 (increasing loops) with an overflow bound, and add a guard for negative arr.Length + offset limits; add new tests for these scenarios.

Reviewed changes

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

Show a summary per file
FileDescription
src/coreclr/jit/compiler.hAdds LimitOffset and declares LimitBase() on NaturalLoopIterInfo.
src/coreclr/jit/flowgraph.cppImplements limit peeling into LimitOffset, adds debug printing, and implements LimitBase(); updates limit accessors to use it.
src/coreclr/jit/loopcloning.hExtends LC_Ident with offset and updates equality/printing/constructors.
src/coreclr/jit/loopcloning.cppMaterializes LC_Ident offsets, enables span stride>1 cloning with an overflow guard, and threads LimitOffset into emitted conditions.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csprojNew JIT test project for span non-unit stride cloning scenarios.
src/tests/JIT/opt/Cloning/SpanNonUnitStride.csNew tests covering span loops with stride 2/3 and various comparison forms.
src/tests/JIT/opt/Cloning/OffsetLimit.csprojNew JIT test project for limit-offset recognition scenarios.
src/tests/JIT/opt/Cloning/OffsetLimit.csNew tests covering Length - K, SIMD-count offsets, and related limit forms.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Morph normally folds `x + 0` / `x - 0` before MatchLimit runs, but if
such a tree slips through the peel would set HasArrayLengthLimit /
HasInvariantLocalLimit based on the peeled base while LimitOffset
stayed 0 -- then LimitBase() would short-circuit to the original
GT_ADD tree and trip the GT_ARR_LENGTH / GT_LCL_VAR asserts in
ArrLenLimit / VarLimit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tannergooding

Copy link
Copy Markdown
Member

Will this also work for cases like:

for(inti=0;i>=0&&i<arr.Length;i+=K){ ...}// or its functional equivalentfor(inti=0;(uint)i<(uint)arr.Length;i+=K){ ...}

In both cases, overflow can happen but the guard check ensures that i cannot be 0, so it must also be safe and so the arr.Length - K shouldn't be required.

@AndyAyersMS

AndyAyersMS commented Jun 11, 2026

Copy link
Copy Markdown
MemberAuthor

We already handled arrays with K less than 58 because the max array length limits limit when overflow can happen (I'll double check for your cases just to be sure).

This PR enables something similar for span where there aren't those same guarantees. (will edit the commit comment).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Note

AI-generated comment.

Prototyped the IV no-overflow analysis as a follow-up (draft branch iv-no-overflow-from-dom). The new code does let SCEV prove the fast clone's IV cannot overflow using the cloning guard, but it does not yet unblock strength reduction or downcounting on stride>1 loops: SCEV's trip-count formula at scev.cpp:2093 still requires |step| == 1 (the standing TODO about adding a division operator), and a secondary lower-bound proof (step <= rhs + 1) goes via optRelopImpliesRelop which doesn't bridge additive transforms. Parking the change until those two are addressed.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Turns out we can't yet handle

 for (int i = 0; i >= 0 && i < arr.Length; i += K) { ... }

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

The other case is handled provided K <= 57. I suppose we could now extend it (and arrays in general to larger K with extra cloning checks like we're doing here for spans).

I will do that as a follow-on PR.

@tannergooding

Copy link
Copy Markdown
Member

Just to be clear, I don't think this is pressing for this PR, more just an interest as to what we are and aren't covering at this point.

because the loop has two exit edges when we analyze it for cloning. Do you think this pattern is common?

I'm not sure exactly how common it is, but it's one of the patterns that I've seen users write for SIMD code.

In general I'd prefer if users didn't have to write stuff like (uint)i <= (uint)span.Length, as I think its functionally harder to reason about than i >= 0 && i < span.Length, where you don't have to think about "this is two's complement, so negatives become large positives, so it is safe".

But, due to historical JIT pessimizations, we have a lot of our own code and have pushed users towards the casting pattern instead. I think we even have a pass that transforms the i >= 0 && i < span.Length check into (uint)i <= (uint)span.Length in the JIT, but it might be happening after we do the loop checks.

Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp Outdated

@tannergoodingtannergooding 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 look good/correct to me. Left a small comment about a potential future opt (not a priority) and a question about whether a given handling path is necessary since morph should canonicalize most trees.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

diffs

More like 300 methods impacted. More cloning, more "unprofitable rejected" cloning.

Morph canonicalizes commutative ops to put any constant on op2 and folds base + 0,
so the cns + base branch in the limit-offset peel is unreachable. Remove it and
update the comment to cite the morph invariants we're relying on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 12, 2026 01:33

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/tests/JIT/opt/Cloning/OffsetLimit.cs
Fix the 'thelimit' typo in optDeriveLoopCloningConditions and add a short-array
case to OffsetLimit asserting IndexOutOfRangeException, locking in the new
arr.Length + offset >= 0 guard against bounds-check elision in the fast clone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Failures are #129027, #129329

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

Reviewed flow graph and loop cloning changes. LGTM on adding the new LimitOffset to make loops for span clonable. It checks (span.Length >= K and no-overflow on i + stride) for Span.

@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

/ba-g known failures

@AndyAyersMS
AndyAyersMS merged commit a4094ea into dotnet:mainJun 12, 2026
141 of 145 checks passed
AndyAyersMS added a commit that referenced this pull request Jun 16, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 17, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Enable cloning of loops over Span<T> when the stride `s` is greater than
1, guarded by a runtime `limit <= INT_MAX - s + 1` condition on the
increasing var-limit case. Other forms are already implicitly safe.
Extend MatchLimit to peel a constant offset off the limit so loops like ```C#
for (int i = 0; i < span.Length - K; i += K) { ... }
```
become clonable. Add a LimitOffset to NaturalLoopIterInfo that feeds
into LC_Ident, the zero-trip / per-access / NE / overflow guards, and a
new `arr.Length + offset >= 0` guard (for very short array lengths and
negative offsets).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Drop the blanket stride cap (`stride < 58`) in
optDeriveLoopCloningConditions that previously rejected array loops
whose post-step IV could exceed INT_MAX given Array.MaxLength. For
larger strides emit a runtime `arr.Length <= INT_MAX - s + 1` cloning
condition; for small strides the implicit Array.MaxLength bound still
suffices and no extra check is added.
Builds on similar work we added for spans in #129309.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 18, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR 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

@AndyAyersMS@tannergooding@JulieLeeMSFT