Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); JIT: generalize cloning conditions slightly by AndyAyersMS · Pull Request #128532 · dotnet/runtime · GitHub
Skip to content

JIT: generalize cloning conditions slightly - #128532

Merged
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard
May 26, 2026
Merged

JIT: generalize cloning conditions slightly#128532
AndyAyersMS merged 11 commits into
dotnet:mainfrom
AndyAyersMS:CloneZeroTripGuard

Conversation

@AndyAyersMS

@AndyAyersMSAndyAyersMS commented May 24, 2026

Copy link
Copy Markdown
Member

Generalize loop cloning a bit:

  • allow more statements between increment and test
  • allow cloning conditions to establish a zero trip test if one is missing.

AndyAyersMSand others added 3 commits May 23, 2026 18:03
optExtractTestIncr previously required the IV increment to be the
immediate predecessor statement of the loop test (after an optional
BBINSTR profile-counter store). This rejected loops where unrelated
statements happened to sit between the increment and the test, even
though the increment-and-test pair was otherwise well-formed.
Relax the check to scan backward through the exit block's statements
for the first IV-shaped update (the shape recognized by
optIsLoopIncrTree). Picking the wrong candidate is safe: the caller
FlowGraphNaturalLoop::AnalyzeIteration verifies via MatchLimit that
the test actually uses the picked iterVar, and via VisitDefs that
there are no other defs of iterVar anywhere in the loop, so any
incorrect pick is rejected and the loop simply fails IV analysis as
before.
This recovers some loop cloning that was previously blocked by
incidental adjacency failures (e.g. an unrelated store sneaking in
between the increment and the test in the exit block).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some loops are rejected by FlowGraphNaturalLoop::CheckLoopConditionBaseCase
because their loop test cannot be proved true on the first iteration, and
no BBJ_COND zero-trip guard exists on the unique-pred chain in front of
the preheader. Previously, unconditional loop inversion happened to supply
such a guard as a side effect; now that inversion is being made more
selective, fewer loops have it.
Teach loop cloning to emit its own runtime zero-trip guard as one of the
fast-path entry conditions. AnalyzeIteration gains an allowMissingBaseCase
flag (off by default to preserve unroller/inversion semantics); when set,
it accepts loops with concrete enough init/limit info and marks
NaturalLoopIterInfo::NeedsZeroTripGuard. optDeriveLoopCloningConditions
then pushes an additional 'init TestOper limit' condition (handling const,
invariant-local, and array-length limits) onto the loop's cloning
conditions, so the fast path runs only when the loop would actually
iterate at least once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a loop has no statically-known init but has a recognizable IV with
a const, invariant-local, or array-length limit, emit the runtime
zero-trip guard using the IterVar local read at the preheader as the
init expression. AnalyzeIteration has already verified the IV is not
address-exposed and has no extraneous defs inside the loop, so the
preheader read yields the value the IV will have on first iteration.
This recovers most of the residual clone loss from the inversion-skip:
on osx-arm64 vs upstream main, LoopsCloned now improves by +164 (up
from +59 before this commit).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 02:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 24, 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.

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 broadens the JIT’s loop-iteration analysis and loop-cloning eligibility by (1) relaxing how the IV increment is located in an exiting block and (2) allowing AnalyzeIteration to succeed even when it can’t prove the loop executes at least once, by deferring that requirement to loop cloning via an explicit runtime “zero-trip” guard condition.

Changes:

  • Update optExtractTestIncr to search backward for an IV-shaped increment instead of requiring it to be immediately adjacent to the loop test.
  • Extend FlowGraphNaturalLoop::AnalyzeIteration with an allowMissingBaseCase mode that sets NeedsZeroTripGuard when the base-case can’t be proven.
  • In loop cloning, when NeedsZeroTripGuard is set, emit an extra cloning condition guarding the fast path against zero-trip execution.

Reviewed changes

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

FileDescription
src/coreclr/jit/optimizer.cppChanges IV increment extraction to scan backward from the loop test.
src/coreclr/jit/loopcloning.cppAdds emission of a zero-trip guard cloning condition and enables missing-base-case iteration analysis for cloning.
src/coreclr/jit/flowgraph.cppAdds allowMissingBaseCase parameter and sets NeedsZeroTripGuard when base-case can’t be proven but a symbolic guard is expressible.
src/coreclr/jit/compiler.hExtends NaturalLoopIterInfo and updates AnalyzeIteration signature/defaults.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
- optExtractTestIncr: after relaxing the adjacency requirement, validate
that no statement strictly between the chosen IV increment and the
loop test references the iterator variable. This restores the
AnalyzeIteration invariant that no loop-body IR observes the
post-increment value of the iterator (except the test itself), which
loop cloning and other consumers rely on.
- AnalyzeIteration: update the Remarks block to document the relaxed
adjacency rule, the new allowMissingBaseCase parameter, and the
caller's obligation to emit a runtime zero-trip guard when
NeedsZeroTripGuard is set.
SPMI (osx-arm64) shows the use-check costs only ~9 of ~95 recovered
clones across the major collections; net cloning gain is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:06

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
Statement::LocalsTreeList asserts NodeThreading::AllLocals, which is
not the threading mode in effect when loop cloning runs (asserts fired
in checked builds, e.g. during 'Clone loops' on
System.Decimal:FromOACurrency).
Replace the iteration over LocalsTreeList with an explicit
fgWalkTreePre walk (lclVarsOnly), which works in any threading mode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- optExtractTestIncr: replace the custom fgWalkTreePre + lambda with
gtTreeHasLocalRead / gtTreeHasLocalStore. Avoids duplicating walker
logic and the per-iteration lambda allocation.
- optDeriveLoopCloningConditions: when both NeedsZeroTripGuard and
HasArrayLengthLimit are set, hoist the ArrIndex allocation and the
array deref entry out so the zero-trip guard and the regular limit
conditions share a single ArrIndex/deref rather than allocating and
pushing them twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 24, 2026 16:26
AnalyzeIteration's VisitDefs already rejects any def of iterVar in the
loop other than the picked increment, so a statement between incr and
test cannot store to iterVar. Only a read check is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
gtTreeHasLocalRead only looks at direct local references, so it cannot
see intervening accesses to an address-exposed iterator through
indirection. AnalyzeIteration ultimately rejects address-exposed IVs
anyway, so reject here and let the caller try the next candidate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS
AndyAyersMS marked this pull request as ready for review May 25, 2026 00:29
CopilotAI review requested due to automatic review settings May 25, 2026 00:29
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Diffs

More cloning (unsurprisingly). Should mitigate most of the cloning losses seen in #128459

@jakobbotsch PTAL
fyi @dotnet/jit-contrib

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/flowgraph.cpp
In response to PR feedback: the backward scan in optExtractTestIncr
picked the first IV-shaped statement and bailed entirely if it failed
later validation (address-exposed iterVar, test does not read iterVar,
or intervening read of iterVar). Restructure the scan so that each
candidate is fully validated inline and rejection simply continues the
scan, only failing when no candidate in the block qualifies.
Also reset NeedsZeroTripGuard at AnalyzeIteration entry so a stale
value cannot leak if the iter-info struct is reused across loops.
SPMI on osx-arm64: +127 LoopsCloned vs upstream main
(aspnet2 +7, libraries.pmi +42, benchmarks.run +28,
libraries.crossgen2 +36, realworld.run +14).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

Copy link
Copy Markdown
MemberAuthor

Latest diffs

Comment threadsrc/coreclr/jit/optimizer.cpp
Comment threadsrc/coreclr/jit/optimizer.cpp Outdated
@tannergooding

Copy link
Copy Markdown
Member

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

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

LGTM, couple of paranoia questions

In response to PR feedback:
* Add a small budget (100) decremented across the outer scan and any
inner intervening-read scan, to prevent pathological O(N^2) behavior
in test blocks with many IV-shaped statements.
* If the test block is in a try region, treat any intervening
statement that may throw (GTF_EXCEPT) as if it were an intervening
read of the iterator. An EH handler could otherwise observe the
post-increment value, violating AnalyzeIteration's invariant.
SPMI on osx-arm64 unchanged: still +127 LoopsCloned vs upstream main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 26, 2026 18:35

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 4 out of 4 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/compiler.h Outdated
Comment threadsrc/coreclr/jit/flowgraph.cpp
Comment threadsrc/coreclr/jit/loopcloning.cpp Outdated
Comment threadsrc/coreclr/jit/loopcloning.cpp
In response to PR feedback. The condition that triggers
NeedsZeroTripGuard is that the loop condition [IterVar TestOper Limit]
cannot be proven to hold on entry, not that the loop body cannot be
proven to execute at least once. A do/while-style loop is guaranteed
to execute at least once but may still need a guard if its condition
is not provably true on entry. Updated wording in compiler.h,
flowgraph.cpp, and loopcloning.cpp to reflect the actual contract.
No code changes; comments only.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AndyAyersMS

AndyAyersMS commented May 26, 2026

Copy link
Copy Markdown
MemberAuthor

We have a few diffs like this:

-G_M56737_IG06: ; bbWeight=4, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz- movzx r8, word ptr [rbx+2*rcx]+G_M56737_IG06: ; bbWeight=3.96, gcrefRegs=0000 {}, byrefRegs=000C {rdx rbx}, byref, isz+ mov r8d, ecx+ movzx r8, word ptr [rbx+2*r8]

I don't think its blocking or anything, but it might be interesting to look at specifically since we can presumably still consume rcx directly here (nothing else mutates it), so possibly just a minor missing cast or containment opportunity

Seems to be a few instances and coule reduce the regression seen by the diffs.

We are apparently running into cases where IV opts, cloning, and LSRA are not playing together nicely:

if (stmt->GetRootNode()->AsLclVarCommon()->GetLclNum() == lclNum)
{
JITDUMP(" V%02u has a phi [%06u] in "FMT_LP"'s header "FMT_BB"\n", lclNum,
dspTreeID(stmt->GetRootNode()), otherLoop->GetIndex(), otherLoop->GetHeader()->bbNum);
// TODO-CQ: We can legally widen these cases, but LSRA is
// unhappy about some of the lifetimes we create when we do
// this. This particularly affects cloned loops.
returnfalse;

This came from the initial SCEV work (#97865) and I don't think @jakobbotsch ever opened a follow-up issue.

@AndyAyersMS
AndyAyersMS merged commit 5341a84 into dotnet:mainMay 26, 2026
135 of 139 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone May 27, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 27, 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@jakobbotsch