[llvm] Tighten loop guards range check idiom (PR #210387)

Aleksandr Popov via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 17 10:49:44 PDT 2026


https://github.com/aleks-tmb created https://github.com/llvm/llvm-project/pull/210387

None

>From 194b9421d682c033c9fad5f6c4d1574bbf5b0c95 Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Fri, 17 Jul 2026 17:01:31 +0000
Subject: [PATCH 1/3] [SCEV][NFC] Precommit test for MatchRangeCheckIdiom
 tightening

---
 .../max-backedge-taken-count-guard-info.ll    | 43 +++++++++++++++++++
 1 file changed, 43 insertions(+)

diff --git a/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll b/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
index bab3a28d1e8a4..42138ee54aab0 100644
--- a/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
+++ b/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
@@ -1656,3 +1656,46 @@ for.body:                                         ; preds = %for.body, %entry
 for.end:                                          ; preds = %for.body
   ret void
 }
+
+; Two range-check idioms on the same variable.
+; The first constrains %N to [1, 101).
+; The second (`5 + %N u< 8`) has a raw range that wraps in the unsigned representation.
+;
+; TODO: Intersecting the wrapping raw range with %N already-known [1, 101) range
+; would narrow it to [1, 3) and tighten the trip count.
+define void @range_check_idiom_tighten_wrapping(i32 %N) {
+; CHECK-LABEL: 'range_check_idiom_tighten_wrapping'
+; CHECK-NEXT:  Classifying expressions for: @range_check_idiom_tighten_wrapping
+; CHECK-NEXT:    %N.off1 = add i32 %N, -1
+; CHECK-NEXT:    --> (-1 + %N) U: full-set S: full-set
+; CHECK-NEXT:    %N.off2 = add i32 %N, 5
+; CHECK-NEXT:    --> (5 + %N) U: full-set S: full-set
+; CHECK-NEXT:    %iv = phi i32 [ 0, %next ], [ %iv.next, %loop ]
+; CHECK-NEXT:    --> {0,+,1}<nuw><nsw><%loop> U: [0,100) S: [0,100) Exits: (-1 + %N) LoopDispositions: { %loop: Computable }
+; CHECK-NEXT:    %iv.next = add nuw nsw i32 %iv, 1
+; CHECK-NEXT:    --> {1,+,1}<nuw><nsw><%loop> U: [1,101) S: [1,101) Exits: %N LoopDispositions: { %loop: Computable }
+; CHECK-NEXT:  Determining loop execution counts for: @range_check_idiom_tighten_wrapping
+; CHECK-NEXT:  Loop %loop: backedge-taken count is (-1 + %N)
+; CHECK-NEXT:  Loop %loop: constant max backedge-taken count is i32 99
+; CHECK-NEXT:  Loop %loop: symbolic max backedge-taken count is (-1 + %N)
+; CHECK-NEXT:  Loop %loop: Trip multiple is 1
+;
+entry:
+  %N.off1 = add i32 %N, -1
+  %chk1 = icmp ult i32 %N.off1, 100
+  br i1 %chk1, label %next, label %exit
+
+next:
+  %N.off2 = add i32 %N, 5
+  %chk2 = icmp ult i32 %N.off2, 8
+  br i1 %chk2, label %loop, label %exit
+
+loop:
+  %iv = phi i32 [ 0, %next ], [ %iv.next, %loop ]
+  %iv.next = add nuw nsw i32 %iv, 1
+  %ec = icmp eq i32 %iv.next, %N
+  br i1 %ec, label %exit, label %loop
+
+exit:
+  ret void
+}

>From 74f5638b377346073bd967b28ec088759f2c01ba Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Fri, 17 Jul 2026 15:16:53 +0000
Subject: [PATCH 2/3] [SCEV][NFC] Refactor LoopGuards range check matching

Parameterize MatchRangeCheckIdiom over (Predicate, LHS, RHS) so it can
be reused for other predicates discovered during guard collection.

Hoist AddRewrite / GetMaybeRewritten above it and use them in place of
the inline try_emplace, so range-check rewrites chain onto existing
ones the same way other rewrites do.
---
 llvm/lib/Analysis/ScalarEvolution.cpp | 55 +++++++++++++--------------
 1 file changed, 27 insertions(+), 28 deletions(-)

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 134be6ac097e0..e6450d647da8a 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -16014,11 +16014,29 @@ void ScalarEvolution::LoopGuards::collectFromBlock(
     // SCEV.  In particular, using contextual facts to imply flags is *NOT*
     // legal.  See the scoping rules for flags in the header to understand why.
 
+    // Puts rewrite rule \p From -> \p To into the rewrite map. Also if \p From
+    // and \p FromRewritten are the same (i.e. there has been no rewrite
+    // registered for \p From), then puts this value in the list of rewritten
+    // expressions.
+    auto AddRewrite = [&](const SCEV *From, const SCEV *FromRewritten,
+                          const SCEV *To) {
+      if (From == FromRewritten)
+        ExprsToRewrite.push_back(From);
+      RewriteMap[From] = To;
+    };
+
+    // Checks whether \p S has already been rewritten. In that case returns the
+    // existing rewrite because we want to chain further rewrites onto the
+    // already rewritten value. Otherwise returns \p S.
+    auto GetMaybeRewritten = [&](const SCEV *S) {
+      return RewriteMap.lookup_or(S, S);
+    };
+
     // Check for a condition of the form (-C1 + X < C2).  InstCombine will
     // create this form when combining two checks of the form (X u< C2 + C1) and
     // (X >=u C1).
-    auto MatchRangeCheckIdiom = [&SE, Predicate, LHS, RHS, &RewriteMap,
-                                 &ExprsToRewrite]() {
+    auto MatchRangeCheckIdiom = [&](ICmpInst::Predicate Predicate,
+                                    const SCEV *LHS, const SCEV *RHS) {
       const SCEVConstant *C1;
       const SCEVUnknown *LHSUnknown;
       auto *C2 = dyn_cast<SCEVConstant>(RHS);
@@ -16034,16 +16052,15 @@ void ScalarEvolution::LoopGuards::collectFromBlock(
       // Bail out, unless we have a non-wrapping, monotonic range.
       if (ExactRegion.isWrappedSet() || ExactRegion.isFullSet())
         return false;
-      auto [I, Inserted] = RewriteMap.try_emplace(LHSUnknown);
-      const SCEV *RewrittenLHS = Inserted ? LHSUnknown : I->second;
-      I->second = SE.getUMaxExpr(
-          SE.getConstant(ExactRegion.getUnsignedMin()),
-          SE.getUMinExpr(RewrittenLHS,
-                         SE.getConstant(ExactRegion.getUnsignedMax())));
-      ExprsToRewrite.push_back(LHSUnknown);
+      const SCEV *RewrittenLHS = GetMaybeRewritten(LHSUnknown);
+      const SCEV *RegionMin = SE.getConstant(ExactRegion.getUnsignedMin());
+      const SCEV *RegionMax = SE.getConstant(ExactRegion.getUnsignedMax());
+      const SCEV *ClampedLHS =
+          SE.getUMaxExpr(RegionMin, SE.getUMinExpr(RewrittenLHS, RegionMax));
+      AddRewrite(LHSUnknown, RewrittenLHS, ClampedLHS);
       return true;
     };
-    if (MatchRangeCheckIdiom())
+    if (MatchRangeCheckIdiom(Predicate, LHS, RHS))
       return;
 
     // Do not apply information for constants or if RHS contains an AddRec.
@@ -16056,24 +16073,6 @@ void ScalarEvolution::LoopGuards::collectFromBlock(
       Predicate = CmpInst::getSwappedPredicate(Predicate);
     }
 
-    // Puts rewrite rule \p From -> \p To into the rewrite map. Also if \p From
-    // and \p FromRewritten are the same (i.e. there has been no rewrite
-    // registered for \p From), then puts this value in the list of rewritten
-    // expressions.
-    auto AddRewrite = [&](const SCEV *From, const SCEV *FromRewritten,
-                          const SCEV *To) {
-      if (From == FromRewritten)
-        ExprsToRewrite.push_back(From);
-      RewriteMap[From] = To;
-    };
-
-    // Checks whether \p S has already been rewritten. In that case returns the
-    // existing rewrite because we want to chain further rewrites onto the
-    // already rewritten value. Otherwise returns \p S.
-    auto GetMaybeRewritten = [&](const SCEV *S) {
-      return RewriteMap.lookup_or(S, S);
-    };
-
     const SCEV *RewrittenLHS = GetMaybeRewritten(LHS);
     // Apply divisibility information when computing the constant multiple.
     const APInt &DividesBy =

>From c8ca9fce791b5d3d3c61662a98e4016dac62103d Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Fri, 17 Jul 2026 15:20:02 +0000
Subject: [PATCH 3/3] [SCEV] Tighten MatchRangeCheckIdiom with LHSUnknown's
 known range

---
 llvm/lib/Analysis/ScalarEvolution.cpp             | 15 ++++++++++++---
 .../max-backedge-taken-count-guard-info.ll        |  8 ++++----
 llvm/test/Transforms/LoopUnroll/runtime-loop5.ll  |  2 +-
 .../Transforms/LoopUnrollAndJam/unroll-and-jam.ll |  4 ++--
 4 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index e6450d647da8a..ff3aa3625ffd1 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -16049,10 +16049,19 @@ void ScalarEvolution::LoopGuards::collectFromBlock(
           ConstantRange::makeExactICmpRegion(Predicate, C2->getAPInt())
               .sub(C1->getAPInt());
 
-      // Bail out, unless we have a non-wrapping, monotonic range.
-      if (ExactRegion.isWrappedSet() || ExactRegion.isFullSet())
-        return false;
+      // Tighten the raw range with what we already know about LHSUnknown
+      // from prior guards recorded in RewriteMap, or from SCEV's own range
+      // analysis.
       const SCEV *RewrittenLHS = GetMaybeRewritten(LHSUnknown);
+      ExactRegion = ExactRegion.intersectWith(SE.getUnsignedRange(RewrittenLHS),
+                                              ConstantRange::Unsigned);
+
+      // Bail if the guard is inconsistent with prior facts, or if the range
+      // is still not a monotonic non-wrapping interval after tightening.
+      if (ExactRegion.isEmptySet() || ExactRegion.isWrappedSet() ||
+          ExactRegion.isFullSet())
+        return false;
+
       const SCEV *RegionMin = SE.getConstant(ExactRegion.getUnsignedMin());
       const SCEV *RegionMax = SE.getConstant(ExactRegion.getUnsignedMax());
       const SCEV *ClampedLHS =
diff --git a/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll b/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
index 42138ee54aab0..e68194cd31caa 100644
--- a/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
+++ b/llvm/test/Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll
@@ -1661,7 +1661,7 @@ for.end:                                          ; preds = %for.body
 ; The first constrains %N to [1, 101).
 ; The second (`5 + %N u< 8`) has a raw range that wraps in the unsigned representation.
 ;
-; TODO: Intersecting the wrapping raw range with %N already-known [1, 101) range
+; Intersecting the wrapping raw range with %N already-known [1, 101) range
 ; would narrow it to [1, 3) and tighten the trip count.
 define void @range_check_idiom_tighten_wrapping(i32 %N) {
 ; CHECK-LABEL: 'range_check_idiom_tighten_wrapping'
@@ -1671,12 +1671,12 @@ define void @range_check_idiom_tighten_wrapping(i32 %N) {
 ; CHECK-NEXT:    %N.off2 = add i32 %N, 5
 ; CHECK-NEXT:    --> (5 + %N) U: full-set S: full-set
 ; CHECK-NEXT:    %iv = phi i32 [ 0, %next ], [ %iv.next, %loop ]
-; CHECK-NEXT:    --> {0,+,1}<nuw><nsw><%loop> U: [0,100) S: [0,100) Exits: (-1 + %N) LoopDispositions: { %loop: Computable }
+; CHECK-NEXT:    --> {0,+,1}<nuw><nsw><%loop> U: [0,2) S: [0,2) Exits: (-1 + %N) LoopDispositions: { %loop: Computable }
 ; CHECK-NEXT:    %iv.next = add nuw nsw i32 %iv, 1
-; CHECK-NEXT:    --> {1,+,1}<nuw><nsw><%loop> U: [1,101) S: [1,101) Exits: %N LoopDispositions: { %loop: Computable }
+; CHECK-NEXT:    --> {1,+,1}<nuw><nsw><%loop> U: [1,3) S: [1,3) Exits: %N LoopDispositions: { %loop: Computable }
 ; CHECK-NEXT:  Determining loop execution counts for: @range_check_idiom_tighten_wrapping
 ; CHECK-NEXT:  Loop %loop: backedge-taken count is (-1 + %N)
-; CHECK-NEXT:  Loop %loop: constant max backedge-taken count is i32 99
+; CHECK-NEXT:  Loop %loop: constant max backedge-taken count is i32 1
 ; CHECK-NEXT:  Loop %loop: symbolic max backedge-taken count is (-1 + %N)
 ; CHECK-NEXT:  Loop %loop: Trip multiple is 1
 ;
diff --git a/llvm/test/Transforms/LoopUnroll/runtime-loop5.ll b/llvm/test/Transforms/LoopUnroll/runtime-loop5.ll
index 0cee4e26d3d5d..1a30a4c47c1e0 100644
--- a/llvm/test/Transforms/LoopUnroll/runtime-loop5.ll
+++ b/llvm/test/Transforms/LoopUnroll/runtime-loop5.ll
@@ -93,7 +93,7 @@ define i3 @test(ptr %a, i3 %n) {
 ; UNROLL-4-NEXT:    [[TMP5:%.*]] = load i3, ptr [[ARRAYIDX_3]], align 1
 ; UNROLL-4-NEXT:    [[ADD_3]] = add nsw i3 [[TMP5]], [[ADD_2]]
 ; UNROLL-4-NEXT:    [[INDVARS_IV_NEXT_3]] = add nuw nsw i64 [[INDVARS_IV]], 4
-; UNROLL-4-NEXT:    [[NITER_NEXT_3]] = add i3 [[NITER]], -4
+; UNROLL-4-NEXT:    [[NITER_NEXT_3]] = add nuw i3 [[NITER]], -4
 ; UNROLL-4-NEXT:    [[NITER_NCMP_3:%.*]] = icmp eq i3 [[NITER_NEXT_3]], [[UNROLL_ITER]]
 ; UNROLL-4-NEXT:    br i1 [[NITER_NCMP_3]], label [[FOR_END_LOOPEXIT_UNR_LCSSA:%.*]], label [[FOR_BODY]], !llvm.loop [[LOOP0:![0-9]+]]
 ; UNROLL-4:       for.end.loopexit.unr-lcssa:
diff --git a/llvm/test/Transforms/LoopUnrollAndJam/unroll-and-jam.ll b/llvm/test/Transforms/LoopUnrollAndJam/unroll-and-jam.ll
index 06b2b6e3ed806..27b4143303692 100644
--- a/llvm/test/Transforms/LoopUnrollAndJam/unroll-and-jam.ll
+++ b/llvm/test/Transforms/LoopUnrollAndJam/unroll-and-jam.ll
@@ -28,7 +28,7 @@ define void @test1(i32 %N, i32 %M, ptr noalias nocapture %A, ptr noalias nocaptu
 ; CHECK-NEXT:    [[ADD8_1:%.*]] = add nuw nsw i32 [[I]], 2
 ; CHECK-NEXT:    [[ADD8_2:%.*]] = add nuw nsw i32 [[I]], 3
 ; CHECK-NEXT:    [[ADD8_3]] = add nuw i32 [[I]], 4
-; CHECK-NEXT:    [[NITER_NEXT_3]] = add i32 [[NITER]], 4
+; CHECK-NEXT:    [[NITER_NEXT_3]] = add nuw i32 [[NITER]], 4
 ; CHECK-NEXT:    br label %[[FOR_INNER:.*]]
 ; CHECK:       [[FOR_INNER]]:
 ; CHECK-NEXT:    [[J:%.*]] = phi i32 [ 0, %[[FOR_OUTER]] ], [ [[INC:%.*]], %[[FOR_INNER]] ]
@@ -1128,7 +1128,7 @@ define void @test9(i32 %N, i32 %M, ptr nocapture %A, ptr nocapture readonly %B)
 ; CHECK-NEXT:    [[ADD8_1:%.*]] = add nuw nsw i32 [[I]], 2
 ; CHECK-NEXT:    [[ADD8_2:%.*]] = add nuw nsw i32 [[I]], 3
 ; CHECK-NEXT:    [[ADD8_3]] = add nuw i32 [[I]], 4
-; CHECK-NEXT:    [[NITER_NEXT_3]] = add i32 [[NITER]], 4
+; CHECK-NEXT:    [[NITER_NEXT_3]] = add nuw i32 [[NITER]], 4
 ; CHECK-NEXT:    br label %[[FOR_INNER:.*]]
 ; CHECK:       [[FOR_INNER]]:
 ; CHECK-NEXT:    [[J:%.*]] = phi i32 [ 0, %[[FOR_OUTER]] ], [ [[INC:%.*]], %[[FOR_INNER]] ]



More information about the llvm-commits mailing list