[llvm] [ScalarEvolution] Avoid recursive proof blowup for recurrence starts (PR #225000)

Justin Leong via llvm-commits llvm-commits at lists.llvm.org
Sun Sep 20 22:42:00 PDT 2026


https://github.com/justinleong22 created https://github.com/llvm/llvm-project/pull/225000

Addresses #221391.

The nonrecursive predicate checker calls back into the full prover when comparing recurrence starts. With chains of dependent loops, the proof tree blows up.

This keeps those queries on the nonrecursive path and adds a 17-loop regression.

On Graviton3 compiling x86-64 IR, the reduced case goes from **56.3 s to 23 ms**, with identical assembly. The original Dovecot C input compiles in **0.48 s**; the baseline timed out after 180 s.

Existing test outcomes are unchanged: **1,924 pass, 4 expected failures, 260 unsupported**, across ScalarEvolution, IndVarSimplify, LoopStrengthReduce, and LoopVectorize. The new 17-loop test times out at 60 s on baseline and passes with the patch. An independent 1,280-case native AArch64 execution check also passes; this uses a separate 14-loop harness, not the Dovecot application.

The tradeoff is giving up induction-heavy proofs at this call site. The selected checks found no loss of analysis precision. Full LLVM and Dovecot application suites were not run.

Measurements and test results were retained at `77cb8a9fd7d80e54bad6cbbe8a0e3507c34fe51b`. The patch applies to this draft's base, `c1324c4b3404119e3128874351eebb3400c2b5c0`, but has not been rebuilt or executed at that newer revision. Fresh validation and contributor review remain before requesting maintainer review.

Thanks to @windsunil for the original report and reproducer. This targets the underlying SCEV proof expansion; it is separate from the suggested change to CanonicalizeFreezeInLoops in the issue discussion.

Assisted-by: OpenAI Codex (implementation, tests, evidence analysis, and PR preparation).


>From aea70f24e1fe93b40505e1f92146b550f2b2e98f Mon Sep 17 00:00:00 2001
From: Justin Leong <117539348+justinleong22 at users.noreply.github.com>
Date: Sun, 20 Sep 2026 22:41:50 -0700
Subject: [PATCH] [ScalarEvolution] Avoid recursive proof blowup for recurrence
 starts

Assisted-by: OpenAI Codex
---
 llvm/lib/Analysis/ScalarEvolution.cpp         |  15 +-
 .../addrec-start-compile-time.ll              | 208 ++++++++++++++++++
 2 files changed, 218 insertions(+), 5 deletions(-)
 create mode 100644 llvm/test/Transforms/LoopStrengthReduce/addrec-start-compile-time.ll

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 50e0170b55089..0067f0fe2999b 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -12885,9 +12885,9 @@ static bool IsMinMaxConsistingOf(const SCEV *MaybeMinMaxExpr,
   return is_contained(MinMaxExpr->operands(), Candidate);
 }
 
-static bool IsKnownPredicateViaAddRecStart(ScalarEvolution &SE,
-                                           CmpPredicate Pred, const SCEV *LHS,
-                                           const SCEV *RHS) {
+static bool IsKnownPredicateViaAddRecStart(
+    CmpPredicate Pred, const SCEV *LHS, const SCEV *RHS,
+    function_ref<bool(const SCEV *, const SCEV *)> IsKnown) {
   // If both sides are affine addrecs for the same loop, with equal
   // steps, and we know the recurrences don't wrap, then we only
   // need to check the predicate on the starting values.
@@ -12909,7 +12909,7 @@ static bool IsKnownPredicateViaAddRecStart(ScalarEvolution &SE,
   if (!LAR->getNoWrapFlags(NW) || !RAR->getNoWrapFlags(NW))
     return false;
 
-  return SE.isKnownPredicate(Pred, LStart, RStart);
+  return IsKnown(LStart, RStart);
 }
 
 /// Is LHS `Pred` RHS true because one of them is an AddRec that is known not to
@@ -13196,7 +13196,12 @@ bool ScalarEvolution::isKnownViaNonRecursiveReasoning(CmpPredicate Pred,
   return isKnownPredicateExtendIdiom(Pred, LHS, RHS) ||
          isKnownPredicateViaConstantRanges(Pred, LHS, RHS) ||
          IsKnownPredicateViaMinOrMax(*this, Pred, LHS, RHS) ||
-         IsKnownPredicateViaAddRecStart(*this, Pred, LHS, RHS) ||
+         IsKnownPredicateViaAddRecStart(
+             Pred, LHS, RHS, [&](const SCEV *LStart, const SCEV *RStart) {
+               // Reentering the full predicate prover here can recursively
+               // branch into induction proofs for a chain of recurrences.
+               return isKnownViaNonRecursiveReasoning(Pred, LStart, RStart);
+             }) ||
          IsKnownPredicateViaAddRecMonotonicity(*this, Pred, LHS, RHS) ||
          isKnownPredicateViaNoOverflow(Pred, LHS, RHS);
 }
diff --git a/llvm/test/Transforms/LoopStrengthReduce/addrec-start-compile-time.ll b/llvm/test/Transforms/LoopStrengthReduce/addrec-start-compile-time.ll
new file mode 100644
index 0000000000000..d93d9dccc7403
--- /dev/null
+++ b/llvm/test/Transforms/LoopStrengthReduce/addrec-start-compile-time.ll
@@ -0,0 +1,208 @@
+; RUN: opt -passes=loop-reduce -S < %s | FileCheck %s
+; REQUIRES: x86-registered-target
+;
+; A sequence of loops whose recurrence starts depend on earlier loops.
+; Proving comparisons of the starts must not recursively branch into full
+; induction proofs from the nonrecursive predicate prover (issue #221391).
+; Calls are to a declared function. No poison pointers, null calls,
+; unchecked memory accesses, or wrapping arithmetic are involved.
+target triple = "x86_64-unknown-linux-gnu"
+declare i1 @keep_going(i64)
+define i64 @chain() {
+; CHECK-LABEL: define i64 @chain()
+; CHECK: loop0:
+; CHECK: call i1 @keep_going
+; CHECK: loop16:
+; CHECK: call i1 @keep_going
+; CHECK: ret i64
+entry:
+  br label %loop0
+loop0:
+  %iv0 = phi i64 [0, %entry], [%next0, %cont0]
+  %next0 = add nuw nsw i64 %iv0, 1
+  %go0 = call i1 @keep_going(i64 %iv0)
+  br i1 %go0, label %cont0, label %after0
+cont0:
+  %guard0 = icmp ult i64 %iv0, 65533
+  br i1 %guard0, label %loop0, label %exit
+after0:
+  %start1 = add nuw nsw i64 %iv0, 2
+  br label %loop1
+loop1:
+  %iv1 = phi i64 [%start1, %after0], [%next1, %cont1]
+  %next1 = add nuw nsw i64 %iv1, 1
+  %go1 = call i1 @keep_going(i64 %iv1)
+  br i1 %go1, label %cont1, label %after1
+cont1:
+  %guard1 = icmp ult i64 %iv1, 65533
+  br i1 %guard1, label %loop1, label %exit
+after1:
+  %start2 = add nuw nsw i64 %iv1, 2
+  br label %loop2
+loop2:
+  %iv2 = phi i64 [%start2, %after1], [%next2, %cont2]
+  %next2 = add nuw nsw i64 %iv2, 1
+  %go2 = call i1 @keep_going(i64 %iv2)
+  br i1 %go2, label %cont2, label %after2
+cont2:
+  %guard2 = icmp ult i64 %iv2, 65533
+  br i1 %guard2, label %loop2, label %exit
+after2:
+  %start3 = add nuw nsw i64 %iv2, 2
+  br label %loop3
+loop3:
+  %iv3 = phi i64 [%start3, %after2], [%next3, %cont3]
+  %next3 = add nuw nsw i64 %iv3, 1
+  %go3 = call i1 @keep_going(i64 %iv3)
+  br i1 %go3, label %cont3, label %after3
+cont3:
+  %guard3 = icmp ult i64 %iv3, 65533
+  br i1 %guard3, label %loop3, label %exit
+after3:
+  %start4 = add nuw nsw i64 %iv3, 2
+  br label %loop4
+loop4:
+  %iv4 = phi i64 [%start4, %after3], [%next4, %cont4]
+  %next4 = add nuw nsw i64 %iv4, 1
+  %go4 = call i1 @keep_going(i64 %iv4)
+  br i1 %go4, label %cont4, label %after4
+cont4:
+  %guard4 = icmp ult i64 %iv4, 65533
+  br i1 %guard4, label %loop4, label %exit
+after4:
+  %start5 = add nuw nsw i64 %iv4, 2
+  br label %loop5
+loop5:
+  %iv5 = phi i64 [%start5, %after4], [%next5, %cont5]
+  %next5 = add nuw nsw i64 %iv5, 1
+  %go5 = call i1 @keep_going(i64 %iv5)
+  br i1 %go5, label %cont5, label %after5
+cont5:
+  %guard5 = icmp ult i64 %iv5, 65533
+  br i1 %guard5, label %loop5, label %exit
+after5:
+  %start6 = add nuw nsw i64 %iv5, 2
+  br label %loop6
+loop6:
+  %iv6 = phi i64 [%start6, %after5], [%next6, %cont6]
+  %next6 = add nuw nsw i64 %iv6, 1
+  %go6 = call i1 @keep_going(i64 %iv6)
+  br i1 %go6, label %cont6, label %after6
+cont6:
+  %guard6 = icmp ult i64 %iv6, 65533
+  br i1 %guard6, label %loop6, label %exit
+after6:
+  %start7 = add nuw nsw i64 %iv6, 2
+  br label %loop7
+loop7:
+  %iv7 = phi i64 [%start7, %after6], [%next7, %cont7]
+  %next7 = add nuw nsw i64 %iv7, 1
+  %go7 = call i1 @keep_going(i64 %iv7)
+  br i1 %go7, label %cont7, label %after7
+cont7:
+  %guard7 = icmp ult i64 %iv7, 65533
+  br i1 %guard7, label %loop7, label %exit
+after7:
+  %start8 = add nuw nsw i64 %iv7, 2
+  br label %loop8
+loop8:
+  %iv8 = phi i64 [%start8, %after7], [%next8, %cont8]
+  %next8 = add nuw nsw i64 %iv8, 1
+  %go8 = call i1 @keep_going(i64 %iv8)
+  br i1 %go8, label %cont8, label %after8
+cont8:
+  %guard8 = icmp ult i64 %iv8, 65533
+  br i1 %guard8, label %loop8, label %exit
+after8:
+  %start9 = add nuw nsw i64 %iv8, 2
+  br label %loop9
+loop9:
+  %iv9 = phi i64 [%start9, %after8], [%next9, %cont9]
+  %next9 = add nuw nsw i64 %iv9, 1
+  %go9 = call i1 @keep_going(i64 %iv9)
+  br i1 %go9, label %cont9, label %after9
+cont9:
+  %guard9 = icmp ult i64 %iv9, 65533
+  br i1 %guard9, label %loop9, label %exit
+after9:
+  %start10 = add nuw nsw i64 %iv9, 2
+  br label %loop10
+loop10:
+  %iv10 = phi i64 [%start10, %after9], [%next10, %cont10]
+  %next10 = add nuw nsw i64 %iv10, 1
+  %go10 = call i1 @keep_going(i64 %iv10)
+  br i1 %go10, label %cont10, label %after10
+cont10:
+  %guard10 = icmp ult i64 %iv10, 65533
+  br i1 %guard10, label %loop10, label %exit
+after10:
+  %start11 = add nuw nsw i64 %iv10, 2
+  br label %loop11
+loop11:
+  %iv11 = phi i64 [%start11, %after10], [%next11, %cont11]
+  %next11 = add nuw nsw i64 %iv11, 1
+  %go11 = call i1 @keep_going(i64 %iv11)
+  br i1 %go11, label %cont11, label %after11
+cont11:
+  %guard11 = icmp ult i64 %iv11, 65533
+  br i1 %guard11, label %loop11, label %exit
+after11:
+  %start12 = add nuw nsw i64 %iv11, 2
+  br label %loop12
+loop12:
+  %iv12 = phi i64 [%start12, %after11], [%next12, %cont12]
+  %next12 = add nuw nsw i64 %iv12, 1
+  %go12 = call i1 @keep_going(i64 %iv12)
+  br i1 %go12, label %cont12, label %after12
+cont12:
+  %guard12 = icmp ult i64 %iv12, 65533
+  br i1 %guard12, label %loop12, label %exit
+after12:
+  %start13 = add nuw nsw i64 %iv12, 2
+  br label %loop13
+loop13:
+  %iv13 = phi i64 [%start13, %after12], [%next13, %cont13]
+  %next13 = add nuw nsw i64 %iv13, 1
+  %go13 = call i1 @keep_going(i64 %iv13)
+  br i1 %go13, label %cont13, label %after13
+cont13:
+  %guard13 = icmp ult i64 %iv13, 65533
+  br i1 %guard13, label %loop13, label %exit
+after13:
+  %start14 = add nuw nsw i64 %iv13, 2
+  br label %loop14
+loop14:
+  %iv14 = phi i64 [%start14, %after13], [%next14, %cont14]
+  %next14 = add nuw nsw i64 %iv14, 1
+  %go14 = call i1 @keep_going(i64 %iv14)
+  br i1 %go14, label %cont14, label %after14
+cont14:
+  %guard14 = icmp ult i64 %iv14, 65533
+  br i1 %guard14, label %loop14, label %exit
+after14:
+  %start15 = add nuw nsw i64 %iv14, 2
+  br label %loop15
+loop15:
+  %iv15 = phi i64 [%start15, %after14], [%next15, %cont15]
+  %next15 = add nuw nsw i64 %iv15, 1
+  %go15 = call i1 @keep_going(i64 %iv15)
+  br i1 %go15, label %cont15, label %after15
+cont15:
+  %guard15 = icmp ult i64 %iv15, 65533
+  br i1 %guard15, label %loop15, label %exit
+after15:
+  %start16 = add nuw nsw i64 %iv15, 2
+  br label %loop16
+loop16:
+  %iv16 = phi i64 [%start16, %after15], [%next16, %cont16]
+  %next16 = add nuw nsw i64 %iv16, 1
+  %go16 = call i1 @keep_going(i64 %iv16)
+  br i1 %go16, label %cont16, label %after16
+cont16:
+  %guard16 = icmp ult i64 %iv16, 65533
+  br i1 %guard16, label %loop16, label %exit
+after16:
+  ret i64 %iv16
+exit:
+  ret i64 65534
+}



More information about the llvm-commits mailing list