[llvm] [SCEV] Look through back-edge selects in `createAddRecFromPHI` (PR #190726)

via llvm-commits llvm-commits at lists.llvm.org
Mon Apr 6 20:55:47 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-analysis

Author: Andy Dienes (adienes)

<details>
<summary>Changes</summary>

disclaimer: until now I have only interacted with LLVM incidentally as it is used in Julia. I came across this while investigating bad performance of some Julia code. This PR (and my understanding of it 🙂) was co-developed with Claude Opus 4.6. I have done my very best to audit it, and this description is in my own words, which based on my unfamiliarity with this stuff are liable to be clunky or wrong.

A front end (here: Julia) https://godbolt.org/z/fYsv1G4qT may emit a single-block loop with a loop-carried value flowing through a select guarded by the exit condition. in the example above that's the `value_phi1759` receiving `spec.select64`. continuing with this example, on the back-edge, ` %.not.not56` must be false, so `%spec.select64 == %9`.  This PR teaches `createAddRecFromPHI` to look through the select to recognize `%9` as a step expression and thus `value_phi1759` as an `AddRec`.

This is done by comparing the branch condition to the select condition. what we'd really want to know is if there is a general implication `Cond => BrCond` or `!Cond => BrCond`, but we only check a few primitive forms of this directly.

I also applied this patch to Julia's fork of LLVM and verified that
* the resulting `@<!-- -->code_native` is better optimized as expected

<details>
<summary>Before (11 instructions)</summary>

```
LBB0_4:                                 ; %L70
                                        ; =>This Inner Loop Header: Depth=1
	mov	x14, x13
	mov	x15, x0
	sub	x10, x10, #<!-- -->1
	cbz	x11, LBB0_6
; %bb.5:                                ; %L70
                                        ;   in Loop: Header=BB0_4 Depth=1
	add	x13, x14, x12
	add	x0, x13, x15
	add	x12, x12, #<!-- -->1
	add	x13, x14, x9
	sub	x11, x11, #<!-- -->1
	cmp	x14, x8
	b.ne	LBB0_4

```
</details>

<details>
<summary>After (8 instructions)</summary>

```
LBB0_4:                                 ; %L70
                                        ; =>This Inner Loop Header: Depth=1
; │ @ REPL[18]:8 within `my_zip_accum`
; │┌ @ essentials.jl:1222 within `+`
	add	x0, x10, x0
; │└
; │ @ REPL[18]:7 within `my_zip_accum`
	cmp	x8, x12
	b.eq	LBB0_6
; %bb.5:                                ; %L70
                                        ;   in Loop: Header=BB0_4 Depth=1
	mov	x14, x13
; │ @ REPL[18] within `my_zip_accum`
	add	x12, x12, #<!-- -->1
; │ @ REPL[18]:7 within `my_zip_accum`
	sub	x13, x13, x9
	add	x10, x10, x11
	cbnz	x14, LBB0_4
```
</details>

* The performance improves ~20% on the motivating iteration example
<details>
<summary>mini benchmark</summary>

```
@<!-- -->inline function my_zip_iterate(i1, i2, s1, s2)
    r1 = iterate(i1, s1); r1 === nothing && return nothing
    r2 = iterate(i2, s2); r2 === nothing && return nothing
    return ((r1[1], r2[1]), (r1[2], r2[2]))
end

function my_zip_accum(A, B)
    s = 0
    r1 = iterate(A); r2 = iterate(B)
    (r1 === nothing || r2 === nothing) && return s
    s += r1[1] + r2[1]
    r = my_zip_iterate(A, B, r1[2], r2[2])
    while r !== nothing
        (a, b), (sa, sb) = r; s += a + b
        r = my_zip_iterate(A, B, sa, sb)
    end; s
end

N = 10_000; ot = Base.OneTo(N); sr = 1:2:2N

using BenchmarkTools
@<!-- -->benchmark my_zip_accum($ot, $sr)

# master
BenchmarkTools.Trial: 10000 samples with 6 evaluations per sample.
 Range (min … max):  5.528 μs …  21.458 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     5.556 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   5.611 μs ± 463.864 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

# PR
BenchmarkTools.Trial: 10000 samples with 7 evaluations per sample.
 Range (min … max):  4.119 μs …  10.649 μs  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     4.577 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   4.598 μs ± 277.535 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%
```
</details>

---
Full diff: https://github.com/llvm/llvm-project/pull/190726.diff


2 Files Affected:

- (modified) llvm/lib/Analysis/ScalarEvolution.cpp (+46) 
- (added) llvm/test/Analysis/ScalarEvolution/select-poison-backedge.ll (+73) 


``````````diff
diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 2862acfedb91d..cbeef433edf60 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -5969,6 +5969,52 @@ const SCEV *ScalarEvolution::createAddRecFromPHI(PHINode *PN) {
   assert(ValueExprMap.find_as(PN) == ValueExprMap.end() &&
          "PHI node already processed?");
 
+  // May encounter single-block loops where the back-edge
+  // value flows through a select guarded by the exit condition.
+  // On the back-edge that condition has a known value, so we can look
+  // through the select to expose the recurrence.
+  if (auto *SI = dyn_cast<SelectInst>(BEValueV)) {
+    Value *Cond = SI->getCondition();
+    if (BasicBlock *Latch = L->getLoopLatch()) {
+      if (auto *BI = dyn_cast<BranchInst>(Latch->getTerminator())) {
+        if (BI->isConditional()) {
+          Value *BrCond = BI->getCondition();
+          bool ExitOnTrue = !L->contains(BI->getSuccessor(0));
+
+          // Figure out which truth value of Cond forces an exit.
+          bool CondTrueImpliesExit = false;
+          bool CondFalseImpliesExit = false;
+
+          if (Cond == BrCond) {
+            CondTrueImpliesExit = ExitOnTrue;
+            CondFalseImpliesExit = !ExitOnTrue;
+          } else if (match(Cond, m_Not(m_Specific(BrCond)))) {
+            CondTrueImpliesExit = !ExitOnTrue;
+            CondFalseImpliesExit = ExitOnTrue;
+          } else if (ExitOnTrue) {
+            // BrCond = Cond | Other, so Cond == true forces exit.
+            Value *Other;
+            if (match(BrCond, m_c_LogicalOr(m_Specific(Cond), m_Value(Other))))
+              CondTrueImpliesExit = true;
+          } else {
+            // BrCond = Cond & Other, so Cond == false forces exit.
+            Value *Other;
+            if (match(BrCond, m_c_LogicalAnd(m_Specific(Cond), m_Value(Other))))
+              CondFalseImpliesExit = true;
+          }
+
+          if (CondTrueImpliesExit) {
+            // On the back-edge Cond is false, so select yields the false arm.
+            BEValueV = SI->getFalseValue();
+          } else if (CondFalseImpliesExit) {
+            // On the back-edge Cond is true, so select yields the true arm.
+            BEValueV = SI->getTrueValue();
+          }
+        }
+      }
+    }
+  }
+
   // First, try to find AddRec expression without creating a fictituos symbolic
   // value for PN.
   if (auto *S = createSimpleAffineAddRec(PN, BEValueV, StartValueV))
diff --git a/llvm/test/Analysis/ScalarEvolution/select-poison-backedge.ll b/llvm/test/Analysis/ScalarEvolution/select-poison-backedge.ll
new file mode 100644
index 0000000000000..b17cf2bc08679
--- /dev/null
+++ b/llvm/test/Analysis/ScalarEvolution/select-poison-backedge.ll
@@ -0,0 +1,73 @@
+; RUN: opt -passes='print<scalar-evolution>' -disable-output %s 2>&1 | FileCheck %s
+
+; Some frontends emit single-block loops where loop-carried values flow
+; through a select guarded by the exit condition.  SCEV should look through
+; the select on the back-edge to recover the underlying AddRec.
+
+; Test 1: select with poison
+define i64 @select_poison(i64 %n, i64 %start, i64 %stop, i64 %step) {
+entry:
+  %empty = icmp slt i64 %n, 1
+  br i1 %empty, label %exit, label %first
+
+first:
+  %first_sum = add i64 %start, 1
+  %first_done = icmp eq i64 %n, 1
+  %first_val_done = icmp eq i64 %start, %stop
+  %first_exit = or i1 %first_done, %first_val_done
+  %first_val_next = add i64 %start, %step
+  br i1 %first_exit, label %exit, label %body
+
+body:
+  %accum = phi i64 [ %first_sum, %first ], [ %sum,     %body ]
+  %val   = phi i64 [ %first_val_next, %first ], [ %sel.val, %body ]
+  %i     = phi i64 [ 2,         %first ], [ %i.next,  %body ]
+  %ab    = add i64 %val, %i
+  %sum   = add i64 %ab, %accum
+  %done1 = icmp eq i64 %i, %n
+  %i.next = add i64 %i, 1
+  %done2 = icmp eq i64 %val, %stop
+  %either = select i1 %done1, i1 true, i1 %done2
+  %val.next = add i64 %val, %step
+  %sel.val = select i1 %done1, i64 poison, i64 %val.next
+  br i1 %either, label %exit, label %body
+
+exit:
+  %result = phi i64 [ 0, %entry ], [ %first_sum, %first ], [ %sum, %body ]
+  ret i64 %result
+}
+
+; CHECK-LABEL: Classifying expressions for: @select_poison
+; CHECK: %val = phi
+; CHECK-NEXT: -->  {(%start + %step),+,%step}<%body>
+
+; Test 2: select with old value
+define i64 @select_oldval(i64 %n, i64 %start, i64 %stop, i64 %step) {
+entry:
+  %guard = icmp slt i64 %n, 1
+  %stop_guard = icmp eq i64 %start, %stop
+  %no_entry = or i1 %guard, %stop_guard
+  br i1 %no_entry, label %exit, label %body
+
+body:
+  %counter = phi i64 [ 1, %entry ], [ %sel.counter, %body ]
+  %stepper = phi i64 [ %start, %entry ], [ %step.next, %body ]
+  %accum   = phi i64 [ 0, %entry ], [ %sum, %body ]
+  %step.next = add i64 %stepper, %step
+  %ab = add i64 %counter, %step.next
+  %sum = add i64 %ab, %accum
+  %done1 = icmp sge i64 %counter, %n
+  %counter.next = add i64 %counter, 1
+  %sel.counter = select i1 %done1, i64 %counter, i64 %counter.next
+  %done2 = icmp eq i64 %step.next, %stop
+  %either = select i1 %done1, i1 true, i1 %done2
+  br i1 %either, label %exit, label %body
+
+exit:
+  %result = phi i64 [ 0, %entry ], [ %sum, %body ]
+  ret i64 %result
+}
+
+; CHECK-LABEL: Classifying expressions for: @select_oldval
+; CHECK: %counter = phi
+; CHECK-NEXT: -->  {1,+,1}<nuw><%body>

``````````

</details>


https://github.com/llvm/llvm-project/pull/190726


More information about the llvm-commits mailing list