From 792c5c25bc4f2b22f83247c58f7cf40e5255d0ca Mon Sep 17 00:00:00 2001 From: Julie Lee Date: Fri, 5 Jun 2026 14:54:30 -0700 Subject: [PATCH 1/4] JIT: Fix IV opt deleting updates of induction variables live into EH handlers optLocalHasNonLoopUses relied on lvDoNotEnregister to detect locals live into exceptional exits. PR #127932 decoupled DNER from liveness (DNER is now set during LSRA/lowering, after optInductionVariables runs), so an induction variable live into a catch handler was no longer detected and its in-loop self-update was removed by optRemoveUnusedIVs, producing wrong results under full-opt codegen (crossgen2, jitstress, AggressiveOptimization). Add an explicit lvTracked && IsLiveInOutOfHandler() check, since VisitRegularExitBlocks deliberately excludes handler blocks. Update two stale comments referencing the old DNER behavior. Fixes #128392. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/inductionvariableopts.cpp | 17 ++++-- .../JitBlue/Runtime_128392/Runtime_128392.cs | 53 +++++++++++++++++++ .../JIT/Regression/Regression_ro_2.csproj | 1 + 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/tests/JIT/Regression/JitBlue/Runtime_128392/Runtime_128392.cs diff --git a/src/coreclr/jit/inductionvariableopts.cpp b/src/coreclr/jit/inductionvariableopts.cpp index 3384175eee12ae..4503079c04ba9b 100644 --- a/src/coreclr/jit/inductionvariableopts.cpp +++ b/src/coreclr/jit/inductionvariableopts.cpp @@ -433,8 +433,8 @@ void PerLoopInfo::Invalidate(FlowGraphNaturalLoop* loop) // sense that all their predecessors must come from inside the loop. Loop // exit canonicalization guarantees this for regular exit blocks. It is not // guaranteed for exceptional exits, but we do not expect to widen IVs that -// are live into exceptional exits since those are marked DNER which makes it -// unprofitable anyway. +// are live into exceptional exits since those are not register candidates +// (see optWidenPrimaryIV) which makes it unprofitable anyway. // // Note that there may be natural loops that have not had their regular exits // canonicalized at the time when IV opts run, in particular if RBO/assertion @@ -1330,7 +1330,8 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo if (varDsc->lvDoNotEnregister) { - // This filters out locals that may be live into exceptional exits. + // We cannot reason precisely about uses of locals that are not + // enregisterable, so be conservative. return true; } @@ -1340,6 +1341,16 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo return true; } + if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) + { + // The local is live into an EH handler (an exceptional exit). The + // regular exit blocks visited below do not include handlers, and + // EH-live locals are no longer necessarily marked DNER at this point + // (that now happens during LSRA/lowering), so we must check for + // handler liveness explicitly here. + return true; + } + BasicBlockVisit visitResult = loop->VisitRegularExitBlocks([=](BasicBlock* block) { if (optLocalIsLiveIntoBlock(lclNum, block)) { diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_128392/Runtime_128392.cs b/src/tests/JIT/Regression/JitBlue/Runtime_128392/Runtime_128392.cs new file mode 100644 index 00000000000000..9857cfe457ae8e --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_128392/Runtime_128392.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Runtime_128392; + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public static class Runtime_128392 +{ + private static long s_sink; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Process(int x) + { + if (x < 0) + { + throw new Exception("boom"); + } + + s_sink += x; + } + + // The loop counter 'i' is updated inside the try and read in the catch handler. + // Induction-variable optimization must not drop the in-loop update of 'i', since + // it is live into the EH handler; otherwise the handler observes a stale value. + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private static int CountProcessedBeforeThrow(int[] values) + { + int i = 0; + try + { + for (; i < values.Length; i++) + { + Process(values[i]); + } + } + catch (Exception) + { + return i; + } + + return -1; + } + + [Fact] + public static void TestEntryPoint() + { + int[] values = { 0, 1, 2, -1, 4 }; + Assert.Equal(3, CountProcessedBeforeThrow(values)); + } +} diff --git a/src/tests/JIT/Regression/Regression_ro_2.csproj b/src/tests/JIT/Regression/Regression_ro_2.csproj index f8bedd81f9050b..f4bb02ae2ab609 100644 --- a/src/tests/JIT/Regression/Regression_ro_2.csproj +++ b/src/tests/JIT/Regression/Regression_ro_2.csproj @@ -98,6 +98,7 @@ + From dc0f3be9f1f79e18683bb26fb515386fb19bb942 Mon Sep 17 00:00:00 2001 From: Julie Lee <63486087+JulieLeeMSFT@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:31:35 -0700 Subject: [PATCH 2/4] Update src/coreclr/jit/inductionvariableopts.cpp Co-authored-by: Jakob Botsch Nielsen --- src/coreclr/jit/inductionvariableopts.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/inductionvariableopts.cpp b/src/coreclr/jit/inductionvariableopts.cpp index 4503079c04ba9b..859cd698244af2 100644 --- a/src/coreclr/jit/inductionvariableopts.cpp +++ b/src/coreclr/jit/inductionvariableopts.cpp @@ -1344,10 +1344,9 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo if (varDsc->lvTracked && varDsc->IsLiveInOutOfHandler()) { // The local is live into an EH handler (an exceptional exit). The - // regular exit blocks visited below do not include handlers, and - // EH-live locals are no longer necessarily marked DNER at this point - // (that now happens during LSRA/lowering), so we must check for - // handler liveness explicitly here. + // regular exit blocks visited below do not include handlers, and we + // use this as a cheap alternative to checking all EH successors + // of all blocks in the loop. return true; } From 1623ff41e2a19a3cd48e0e002a00881683408455 Mon Sep 17 00:00:00 2001 From: Julie Lee Date: Wed, 10 Jun 2026 09:29:37 -0700 Subject: [PATCH 3/4] JIT: Address PR feedback - remove lvDoNotEnregister check, add DEBUG assert for EH-live IVs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/inductionvariableopts.cpp | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/coreclr/jit/inductionvariableopts.cpp b/src/coreclr/jit/inductionvariableopts.cpp index 859cd698244af2..3cf30c7e1c1709 100644 --- a/src/coreclr/jit/inductionvariableopts.cpp +++ b/src/coreclr/jit/inductionvariableopts.cpp @@ -1328,13 +1328,6 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo return true; } - if (varDsc->lvDoNotEnregister) - { - // We cannot reason precisely about uses of locals that are not - // enregisterable, so be conservative. - return true; - } - if (!varDsc->lvTracked && !varDsc->lvInSsa) { // We do not have liveness we can use for this untracked local. @@ -1368,6 +1361,26 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo return true; } +#ifdef DEBUG + // We currently do not expect to ever widen IVs that are live into + // exceptional exits. Such IVs are not currently register candidates (EH + // write-thru is only for single def locals) which makes it unprofitable. + // If this ever changes we need some more expansive handling here. + loop->VisitLoopBlocks([=](BasicBlock* block) { + block->VisitAllSuccs(this, [=](BasicBlock* succ) { + if (!loop->ContainsBlock(succ) && bbIsHandlerBeg(succ)) + { + assert(!optLocalIsLiveIntoBlock(lclNum, succ) && + "Candidate IV for widening is live into exceptional exit"); + } + + return BasicBlockVisit::Continue; + }); + + return BasicBlockVisit::Continue; + }); +#endif + return false; } From f4c62d80c89e777d68b5455629646da81814723e Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 19 Jun 2026 10:50:41 +0200 Subject: [PATCH 4/4] Rephrase comment --- src/coreclr/jit/inductionvariableopts.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/inductionvariableopts.cpp b/src/coreclr/jit/inductionvariableopts.cpp index 3cf30c7e1c1709..86d0d5e8802b9c 100644 --- a/src/coreclr/jit/inductionvariableopts.cpp +++ b/src/coreclr/jit/inductionvariableopts.cpp @@ -1362,16 +1362,15 @@ bool Compiler::optLocalHasNonLoopUses(unsigned lclNum, FlowGraphNaturalLoop* loo } #ifdef DEBUG - // We currently do not expect to ever widen IVs that are live into - // exceptional exits. Such IVs are not currently register candidates (EH - // write-thru is only for single def locals) which makes it unprofitable. - // If this ever changes we need some more expansive handling here. + // We currently do not expect to optimize locals that are live into exceptional + // exits. Such IVs are not currently register candidates (EH write-thru is + // only for single def locals) which makes it unprofitable. If this ever + // changes we need some more expansive handling here. loop->VisitLoopBlocks([=](BasicBlock* block) { block->VisitAllSuccs(this, [=](BasicBlock* succ) { if (!loop->ContainsBlock(succ) && bbIsHandlerBeg(succ)) { - assert(!optLocalIsLiveIntoBlock(lclNum, succ) && - "Candidate IV for widening is live into exceptional exit"); + assert(!optLocalIsLiveIntoBlock(lclNum, succ) && "Candidate local is live into exceptional exit"); } return BasicBlockVisit::Continue;