Skip to content

[LoopRotate] Use SCEV exit counts to improve rotation profitability#187483

Merged
fhahn merged 4 commits into
llvm:mainfrom
fhahn:loop-rotate-countable
Mar 20, 2026
Merged

[LoopRotate] Use SCEV exit counts to improve rotation profitability#187483
fhahn merged 4 commits into
llvm:mainfrom
fhahn:loop-rotate-countable

Conversation

@fhahn

@fhahn fhahn commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral
https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (+0.15%) and 7zip faster in most cases
(
-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after 5f648c3 which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on #162654.

@llvmbot

llvmbot commented Mar 19, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-llvm-transforms

Author: Florian Hahn (fhahn)

Changes

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral
https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (+0.15%) and 7zip faster in most cases
(
-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after 5f648c3 which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on #162654.


Patch is 24.26 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/187483.diff

9 Files Affected:

  • (modified) llvm/include/llvm/Transforms/Scalar/LoopRotation.h (+2-1)
  • (modified) llvm/include/llvm/Transforms/Utils/LoopRotationUtils.h (+2-1)
  • (modified) llvm/lib/Passes/PassBuilder.cpp (+12-4)
  • (modified) llvm/lib/Passes/PassBuilderPipelines.cpp (+1-1)
  • (modified) llvm/lib/Passes/PassRegistry.def (+5-3)
  • (modified) llvm/lib/Transforms/Scalar/LoopRotation.cpp (+12-6)
  • (modified) llvm/lib/Transforms/Utils/LoopRotationUtils.cpp (+20-9)
  • (added) llvm/test/Transforms/LoopRotate/rotate-exitcount.ll (+124)
  • (added) llvm/test/Transforms/PhaseOrdering/AArch64/loop-rotate-to-enable-unrolling-and-vectorization.ll (+164)
diff --git a/llvm/include/llvm/Transforms/Scalar/LoopRotation.h b/llvm/include/llvm/Transforms/Scalar/LoopRotation.h
index cd108f7383e4c..256345b26d57c 100644
--- a/llvm/include/llvm/Transforms/Scalar/LoopRotation.h
+++ b/llvm/include/llvm/Transforms/Scalar/LoopRotation.h
@@ -24,7 +24,7 @@ class Loop;
 class LoopRotatePass : public PassInfoMixin<LoopRotatePass> {
 public:
   LoopRotatePass(bool EnableHeaderDuplication = true,
-                 bool PrepareForLTO = false);
+                 bool PrepareForLTO = false, bool CheckExitCount = false);
   PreservedAnalyses run(Loop &L, LoopAnalysisManager &AM,
                         LoopStandardAnalysisResults &AR, LPMUpdater &U);
 
@@ -34,6 +34,7 @@ class LoopRotatePass : public PassInfoMixin<LoopRotatePass> {
 private:
   const bool EnableHeaderDuplication;
   const bool PrepareForLTO;
+  const bool CheckExitCount;
 };
 }
 
diff --git a/llvm/include/llvm/Transforms/Utils/LoopRotationUtils.h b/llvm/include/llvm/Transforms/Utils/LoopRotationUtils.h
index c3643e0f27f94..18a1c4efcaab2 100644
--- a/llvm/include/llvm/Transforms/Utils/LoopRotationUtils.h
+++ b/llvm/include/llvm/Transforms/Utils/LoopRotationUtils.h
@@ -37,7 +37,8 @@ LLVM_ABI bool LoopRotation(Loop *L, LoopInfo *LI,
                            DominatorTree *DT, ScalarEvolution *SE,
                            MemorySSAUpdater *MSSAU, const SimplifyQuery &SQ,
                            bool RotationOnly, unsigned Threshold,
-                           bool IsUtilMode, bool PrepareForLTO = false);
+                           bool IsUtilMode, bool PrepareForLTO = false,
+                           bool CheckExitCount = false);
 
 } // namespace llvm
 
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index ea2380448c06f..a23d64b491a79 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -1274,17 +1274,25 @@ Expected<LICMOptions> parseLICMOptions(StringRef Params) {
   return Result;
 }
 
-Expected<std::pair<bool, bool>> parseLoopRotateOptions(StringRef Params) {
-  std::pair<bool, bool> Result = {true, false};
+struct LoopRotateOptions {
+  bool EnableHeaderDuplication = true;
+  bool PrepareForLTO = false;
+  bool CheckExitCount = false;
+};
+
+Expected<LoopRotateOptions> parseLoopRotateOptions(StringRef Params) {
+  LoopRotateOptions Result;
   while (!Params.empty()) {
     StringRef ParamName;
     std::tie(ParamName, Params) = Params.split(';');
 
     bool Enable = !ParamName.consume_front("no-");
     if (ParamName == "header-duplication") {
-      Result.first = Enable;
+      Result.EnableHeaderDuplication = Enable;
     } else if (ParamName == "prepare-for-lto") {
-      Result.second = Enable;
+      Result.PrepareForLTO = Enable;
+    } else if (ParamName == "check-exit-count") {
+      Result.CheckExitCount = Enable;
     } else {
       return make_error<StringError>(
           formatv("invalid LoopRotate pass parameter '{}'", ParamName).str(),
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index 123541a985454..5e4688f4dd7ef 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -1579,7 +1579,7 @@ PassBuilder::buildModuleOptimizationPipeline(OptimizationLevel Level,
   // Disable header duplication at -Oz.
   LPM.addPass(LoopRotatePass(EnableLoopHeaderDuplication ||
                                  Level != OptimizationLevel::Oz,
-                             LTOPreLink));
+                             LTOPreLink, /*CheckExitCount=*/true));
   // Some loops may have become dead by now. Try to delete them.
   // FIXME: see discussion in https://reviews.llvm.org/D112851,
   //        this may need to be revisited once we run GVN before loop deletion
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index ad696861e556c..47c998a78d7c3 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -800,12 +800,14 @@ LOOP_PASS_WITH_PARAMS(
     parseLICMOptions, "allowspeculation;no-allowspeculation")
 LOOP_PASS_WITH_PARAMS(
     "loop-rotate", "LoopRotatePass",
-    [](std::pair<bool, bool> Params) {
-      return LoopRotatePass(Params.first, Params.second);
+    [](LoopRotateOptions Params) {
+      return LoopRotatePass(Params.EnableHeaderDuplication, Params.PrepareForLTO,
+                            Params.CheckExitCount);
     },
     parseLoopRotateOptions,
     "no-header-duplication;header-duplication;"
-    "no-prepare-for-lto;prepare-for-lto")
+    "no-prepare-for-lto;prepare-for-lto;"
+    "no-check-exit-count;check-exit-count")
 LOOP_PASS_WITH_PARAMS(
     "simple-loop-unswitch", "SimpleLoopUnswitchPass",
     [](std::pair<bool, bool> Params) {
diff --git a/llvm/lib/Transforms/Scalar/LoopRotation.cpp b/llvm/lib/Transforms/Scalar/LoopRotation.cpp
index 112761f81016f..50d44369a40d0 100644
--- a/llvm/lib/Transforms/Scalar/LoopRotation.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopRotation.cpp
@@ -38,9 +38,10 @@ static cl::opt<bool> PrepareForLTOOption(
     cl::desc("Run loop-rotation in the prepare-for-lto stage. This option "
              "should be used for testing only."));
 
-LoopRotatePass::LoopRotatePass(bool EnableHeaderDuplication, bool PrepareForLTO)
+LoopRotatePass::LoopRotatePass(bool EnableHeaderDuplication, bool PrepareForLTO,
+                               bool CheckExitCount)
     : EnableHeaderDuplication(EnableHeaderDuplication),
-      PrepareForLTO(PrepareForLTO) {}
+      PrepareForLTO(PrepareForLTO), CheckExitCount(CheckExitCount) {}
 
 void LoopRotatePass::printPipeline(
     raw_ostream &OS, function_ref<StringRef(StringRef)> MapClassName2PassName) {
@@ -53,7 +54,11 @@ void LoopRotatePass::printPipeline(
 
   if (!PrepareForLTO)
     OS << "no-";
-  OS << "prepare-for-lto";
+  OS << "prepare-for-lto;";
+
+  if (!CheckExitCount)
+    OS << "no-";
+  OS << "check-exit-count";
   OS << ">";
 }
 
@@ -74,9 +79,10 @@ PreservedAnalyses LoopRotatePass::run(Loop &L, LoopAnalysisManager &AM,
   std::optional<MemorySSAUpdater> MSSAU;
   if (AR.MSSA)
     MSSAU = MemorySSAUpdater(AR.MSSA);
-  bool Changed = LoopRotation(&L, &AR.LI, &AR.TTI, &AR.AC, &AR.DT, &AR.SE,
-                              MSSAU ? &*MSSAU : nullptr, SQ, false, Threshold,
-                              false, PrepareForLTO || PrepareForLTOOption);
+  bool Changed =
+      LoopRotation(&L, &AR.LI, &AR.TTI, &AR.AC, &AR.DT, &AR.SE,
+                   MSSAU ? &*MSSAU : nullptr, SQ, false, Threshold, false,
+                   PrepareForLTO || PrepareForLTOOption, CheckExitCount);
 
   if (!Changed)
     return PreservedAnalyses::all();
diff --git a/llvm/lib/Transforms/Utils/LoopRotationUtils.cpp b/llvm/lib/Transforms/Utils/LoopRotationUtils.cpp
index bf236d48f58f9..c8bc5e4daeff3 100644
--- a/llvm/lib/Transforms/Utils/LoopRotationUtils.cpp
+++ b/llvm/lib/Transforms/Utils/LoopRotationUtils.cpp
@@ -63,16 +63,18 @@ class LoopRotate {
   bool RotationOnly;
   bool IsUtilMode;
   bool PrepareForLTO;
+  bool CheckExitCount;
 
 public:
   LoopRotate(unsigned MaxHeaderSize, LoopInfo *LI,
              const TargetTransformInfo *TTI, AssumptionCache *AC,
              DominatorTree *DT, ScalarEvolution *SE, MemorySSAUpdater *MSSAU,
              const SimplifyQuery &SQ, bool RotationOnly, bool IsUtilMode,
-             bool PrepareForLTO)
+             bool PrepareForLTO, bool CheckExitCount)
       : MaxHeaderSize(MaxHeaderSize), LI(LI), TTI(TTI), AC(AC), DT(DT), SE(SE),
         MSSAU(MSSAU), SQ(SQ), RotationOnly(RotationOnly),
-        IsUtilMode(IsUtilMode), PrepareForLTO(PrepareForLTO) {}
+        IsUtilMode(IsUtilMode), PrepareForLTO(PrepareForLTO),
+        CheckExitCount(CheckExitCount) {}
   bool processLoop(Loop *L);
 
 private:
@@ -178,11 +180,12 @@ static void RewriteUsesOfClonedInstructions(BasicBlock *OrigHeader,
   }
 }
 
-// Assuming both header and latch are exiting, look for a phi which is only
-// used outside the loop (via a LCSSA phi) in the exit from the header.
-// This means that rotating the loop can remove the phi.
-static bool profitableToRotateLoopExitingLatch(Loop *L) {
+// Assuming both header and latch are exiting, check if rotating is profitable:
+// either a header phi becomes dead, or rotating makes the latch exit count
+// computable (enabling downstream optimizations like unrolling/vectorization).
+static bool profitableToRotateLoopExitingLatch(Loop *L, ScalarEvolution *SE) {
   BasicBlock *Header = L->getHeader();
+  BasicBlock *Latch = L->getLoopLatch();
   CondBrInst *BI = dyn_cast<CondBrInst>(Header->getTerminator());
   BasicBlock *HeaderExit = BI->getSuccessor(0);
   if (L->contains(HeaderExit))
@@ -196,6 +199,13 @@ static bool profitableToRotateLoopExitingLatch(Loop *L) {
       continue;
     return true;
   }
+
+  // Check if rotating would make the latch exit count computable, enabling
+  // optimizations like runtime unrolling and vectorization.
+  if (SE && isa<SCEVCouldNotCompute>(SE->getExitCount(L, Latch)) &&
+      !isa<SCEVCouldNotCompute>(SE->getExitCount(L, Header)))
+    return true;
+
   return false;
 }
 
@@ -363,7 +373,7 @@ bool LoopRotate::rotateLoop(Loop *L, bool SimplifiedLatch) {
   // Rotate if the loop latch was just simplified. Or if it makes the loop exit
   // count computable. Or if we think it will be profitable.
   if (L->isLoopExiting(OrigLatch) && !SimplifiedLatch && IsUtilMode == false &&
-      !profitableToRotateLoopExitingLatch(L))
+      !profitableToRotateLoopExitingLatch(L, CheckExitCount ? SE : nullptr))
     return Rotated;
 
   // Check size of original header and reject loop if it is very big or we can't
@@ -965,8 +975,9 @@ bool llvm::LoopRotation(Loop *L, LoopInfo *LI, const TargetTransformInfo *TTI,
                         ScalarEvolution *SE, MemorySSAUpdater *MSSAU,
                         const SimplifyQuery &SQ, bool RotationOnly = true,
                         unsigned Threshold = unsigned(-1),
-                        bool IsUtilMode = true, bool PrepareForLTO) {
+                        bool IsUtilMode = true, bool PrepareForLTO,
+                        bool CheckExitCount) {
   LoopRotate LR(Threshold, LI, TTI, AC, DT, SE, MSSAU, SQ, RotationOnly,
-                IsUtilMode, PrepareForLTO);
+                IsUtilMode, PrepareForLTO, CheckExitCount);
   return LR.processLoop(L);
 }
diff --git a/llvm/test/Transforms/LoopRotate/rotate-exitcount.ll b/llvm/test/Transforms/LoopRotate/rotate-exitcount.ll
new file mode 100644
index 0000000000000..9acff66150664
--- /dev/null
+++ b/llvm/test/Transforms/LoopRotate/rotate-exitcount.ll
@@ -0,0 +1,124 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt -S -passes='loop(loop-rotate<header-duplication>)' %s | FileCheck %s --check-prefix=CHECK
+
+; Computable header exit, data-dependent latch exit. Rotated with check-exit-count.
+define ptr @search_loop(ptr %begin, ptr %end, i8 %val) {
+; CHECK-LABEL: @search_loop(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[CMP_ENTRY:%.*]] = icmp eq ptr [[BEGIN:%.*]], [[END:%.*]]
+; CHECK-NEXT:    br i1 [[CMP_ENTRY]], label [[EXIT:%.*]], label [[FOR_COND_PREHEADER:%.*]]
+; CHECK:       for.cond.preheader:
+; CHECK-NEXT:    br label [[FOR_COND:%.*]]
+; CHECK:       for.cond:
+; CHECK-NEXT:    [[PTR:%.*]] = phi ptr [ [[PTR_DEC:%.*]], [[FOR_BODY:%.*]] ], [ [[END]], [[FOR_COND_PREHEADER]] ]
+; CHECK-NEXT:    [[PTR_DEC]] = getelementptr inbounds i8, ptr [[PTR]], i64 -1
+; CHECK-NEXT:    [[CMP_END:%.*]] = icmp eq ptr [[PTR_DEC]], [[BEGIN]]
+; CHECK-NEXT:    br i1 [[CMP_END]], label [[NOT_FOUND:%.*]], label [[FOR_BODY]]
+; CHECK:       for.body:
+; CHECK-NEXT:    [[LOAD:%.*]] = load i8, ptr [[PTR_DEC]], align 1
+; CHECK-NEXT:    [[CMP_VAL:%.*]] = icmp eq i8 [[LOAD]], [[VAL:%.*]]
+; CHECK-NEXT:    br i1 [[CMP_VAL]], label [[FOUND:%.*]], label [[FOR_COND]]
+; CHECK:       found:
+; CHECK-NEXT:    [[PTR_DEC_LCSSA1:%.*]] = phi ptr [ [[PTR_DEC]], [[FOR_BODY]] ]
+; CHECK-NEXT:    ret ptr [[PTR_DEC_LCSSA1]]
+; CHECK:       not.found:
+; CHECK-NEXT:    br label [[EXIT]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret ptr [[END]]
+;
+entry:
+  %cmp.entry = icmp eq ptr %begin, %end
+  br i1 %cmp.entry, label %exit, label %for.cond
+
+for.cond:
+  %ptr = phi ptr [ %end, %entry ], [ %ptr.dec, %for.body ]
+  %ptr.dec = getelementptr inbounds i8, ptr %ptr, i64 -1
+  %cmp.end = icmp eq ptr %ptr.dec, %begin
+  br i1 %cmp.end, label %not.found, label %for.body
+
+for.body:
+  %load = load i8, ptr %ptr.dec, align 1
+  %cmp.val = icmp eq i8 %load, %val
+  br i1 %cmp.val, label %found, label %for.cond
+
+found:
+  ret ptr %ptr.dec
+
+not.found:
+  br label %exit
+
+exit:
+  ret ptr %end
+}
+
+; Both exits are memory-dependent (not computable). Not rotated.
+define i32 @both_mem_dependent(ptr %p, ptr %q, i32 %n) {
+; CHECK-LABEL: @both_mem_dependent(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i32 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[BODY:%.*]] ]
+; CHECK-NEXT:    [[LOAD1:%.*]] = load i32, ptr [[P:%.*]], align 4
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[LOAD1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label [[EXIT1:%.*]], label [[BODY]]
+; CHECK:       body:
+; CHECK-NEXT:    [[LOAD2:%.*]] = load i32, ptr [[Q:%.*]], align 4
+; CHECK-NEXT:    [[CMP2:%.*]] = icmp eq i32 [[LOAD2]], 0
+; CHECK-NEXT:    [[IV_NEXT]] = add i32 [[IV]], 1
+; CHECK-NEXT:    br i1 [[CMP2]], label [[EXIT2:%.*]], label [[LOOP]]
+; CHECK:       exit1:
+; CHECK-NEXT:    [[IV_LCSSA:%.*]] = phi i32 [ [[IV]], [[LOOP]] ]
+; CHECK-NEXT:    ret i32 [[IV_LCSSA]]
+; CHECK:       exit2:
+; CHECK-NEXT:    [[IV_LCSSA1:%.*]] = phi i32 [ [[IV]], [[BODY]] ]
+; CHECK-NEXT:    ret i32 [[IV_LCSSA1]]
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i32 [ 0, %entry ], [ %iv.next, %body ]
+  %load1 = load i32, ptr %p, align 4
+  %cmp1 = icmp eq i32 %load1, 0
+  br i1 %cmp1, label %exit1, label %body
+
+body:
+  %load2 = load i32, ptr %q, align 4
+  %cmp2 = icmp eq i32 %load2, 0
+  %iv.next = add i32 %iv, 1
+  br i1 %cmp2, label %exit2, label %loop
+
+exit1:
+  ret i32 %iv
+
+exit2:
+  ret i32 %iv
+}
+
+; Latch is unconditional, already properly structured.
+define void @already_rotated(ptr %p, i32 %n) {
+; CHECK-LABEL: @already_rotated(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i32 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
+; CHECK-NEXT:    store i32 [[IV]], ptr [[P:%.*]], align 4
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw i32 [[IV]], 1
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq i32 [[IV_NEXT]], [[N:%.*]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[EXIT:%.*]], label [[LOOP]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i32 [ 0, %entry ], [ %iv.next, %loop ]
+  store i32 %iv, ptr %p, align 4
+  %iv.next = add nuw i32 %iv, 1
+  %cmp = icmp eq i32 %iv.next, %n
+  br i1 %cmp, label %exit, label %loop
+
+exit:
+  ret void
+}
diff --git a/llvm/test/Transforms/PhaseOrdering/AArch64/loop-rotate-to-enable-unrolling-and-vectorization.ll b/llvm/test/Transforms/PhaseOrdering/AArch64/loop-rotate-to-enable-unrolling-and-vectorization.ll
new file mode 100644
index 0000000000000..e23d1426d7363
--- /dev/null
+++ b/llvm/test/Transforms/PhaseOrdering/AArch64/loop-rotate-to-enable-unrolling-and-vectorization.ll
@@ -0,0 +1,164 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -S -O3 %s | FileCheck %s
+
+target triple = "aarch64-unknown-linux-gnu"
+
+; Test that a search loop with computable header exit gets rotated and
+; runtime-unrolled at -O3.
+define ptr @search_loop_unrolled(ptr %begin, ptr %end, i8 %val) {
+; CHECK-LABEL: define ptr @search_loop_unrolled(
+; CHECK-SAME: ptr readnone captures(address) [[BEGIN:%.*]], ptr readonly captures(address, ret: address, provenance) [[END:%.*]], i8 [[VAL:%.*]]) local_unnamed_addr #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[CMP_ENTRY:%.*]] = icmp eq ptr [[BEGIN]], [[END]]
+; CHECK-NEXT:    [[PTR_DEC1:%.*]] = getelementptr inbounds i8, ptr [[END]], i64 -1
+; CHECK-NEXT:    [[CMP_END2:%.*]] = icmp eq ptr [[PTR_DEC1]], [[BEGIN]]
+; CHECK-NEXT:    [[OR_COND:%.*]] = select i1 [[CMP_ENTRY]], i1 true, i1 [[CMP_END2]]
+; CHECK-NEXT:    br i1 [[OR_COND]], label %[[COMMON_RET:.*]], label %[[FOR_COND:.*]]
+; CHECK:       [[FOR_COND]]:
+; CHECK-NEXT:    [[END5:%.*]] = ptrtoint ptr [[END]] to i64
+; CHECK-NEXT:    [[BEGIN6:%.*]] = ptrtoint ptr [[BEGIN]] to i64
+; CHECK-NEXT:    [[TMP0:%.*]] = xor i64 [[BEGIN6]], -1
+; CHECK-NEXT:    [[TMP1:%.*]] = add i64 [[TMP0]], [[END5]]
+; CHECK-NEXT:    [[TMP2:%.*]] = freeze i64 [[TMP1]]
+; CHECK-NEXT:    [[TMP3:%.*]] = add i64 [[TMP2]], -1
+; CHECK-NEXT:    [[XTRAITER:%.*]] = and i64 [[TMP2]], 3
+; CHECK-NEXT:    [[LCMP_MOD_NOT:%.*]] = icmp eq i64 [[XTRAITER]], 0
+; CHECK-NEXT:    br i1 [[LCMP_MOD_NOT]], label %[[FOR_BODY_PROL_LOOPEXIT:.*]], label %[[FOR_BODY_PROL:.*]]
+; CHECK:       [[FOR_BODY_PROL]]:
+; CHECK-NEXT:    [[PTR_DEC5:%.*]] = phi ptr [ [[PTR_DEC4:%.*]], %[[FOR_COND_PROL:.*]] ], [ [[PTR_DEC1]], %[[FOR_COND]] ]
+; CHECK-NEXT:    [[PROL_ITER:%.*]] = phi i64 [ [[PROL_ITER_NEXT:%.*]], %[[FOR_COND_PROL]] ], [ 0, %[[FOR_COND]] ]
+; CHECK-NEXT:    [[LOAD_PROL:%.*]] = load i8, ptr [[PTR_DEC5]], align 1
+; CHECK-NEXT:    [[CMP_VAL_PROL:%.*]] = icmp eq i8 [[LOAD_PROL]], [[VAL]]
+; CHECK-NEXT:    br i1 [[CMP_VAL_PROL]], label %[[COMMON_RET]], label %[[FOR_COND_PROL]]
+; CHECK:       [[FOR_COND_PROL]]:
+; CHECK-NEXT:    [[PTR_DEC4]] = getelementptr inbounds i8, ptr [[PTR_DEC5]], i64 -1
+; CHECK-NEXT:    [[PROL_ITER_NEXT]] = add i64 [[PROL_ITER]], 1
+; CHECK-NEXT:    [[PROL_ITER_CMP_NOT:%.*]] = icmp eq i64 [[PROL_ITER_NEXT]], [[XTRAITER]]
+; CHECK-NEXT:    br i1 [[PROL_ITER_CMP_NOT]], label %[[FOR_BODY_PROL_LOOPEXIT]], label %[[FOR_BODY_PROL]], !llvm.loop [[LOOP0:![0-9]+]]
+; CHECK:       [[FOR_BODY_PROL_LOOPEXIT]]:
+; CHECK-NEXT:    [[PTR_DEC3_UNR:%.*]] = phi ptr [ [[PTR_DEC1]], %[[FOR_COND]] ], [ [[PTR_DEC4]], %[[FOR_COND_PROL]] ]
+; CHECK-NEXT:    [[TMP4:%.*]] = icmp ult i64 [[TMP3]], 3
+; CHECK-NEXT:    br i1 [[TMP4]], label %[[COMMON_RET]], label %[[FOR_BODY:.*]]
+; CHECK:       [[FOR_COND1:.*]]:
+; CHECK-NEXT:    [[PTR_DEC:%.*]] = getelementptr inbounds i8, ptr [[PTR_DEC3:%.*]], i64 -1
+; CHECK-NEXT:    [[LOAD_1:%.*]] = load i8, ptr [[PTR_DEC]], align 1
+; CHECK-NEXT:    [[CMP_VAL_1:%.*]] = icmp eq i8 [[LOAD_1]], [[VAL]]
+; CHECK-NEXT:    br i1 [[CMP_VAL_1]], label %[[COMMON_RET_LOOPEXIT_UNR_LCSSA_LOOPEXIT_SPLIT_LOOP_EXIT14:.*]], label %[[FOR_COND_1:.*]]
+; CHECK:       [[FOR_COND_1]]:
+; CHECK-NEXT:    [[PTR_DEC_1:%.*]] = getelementptr inbounds i8, ptr [[PTR_DEC3]], i64 -2
+; CHECK-NEXT:    [[LOAD_2:%.*]] = load i8, ptr [[PTR_DEC_1]], align 1
+; CHECK-NEXT:    [[CMP_VAL_2:%.*]] = icmp eq i8 [[LOAD_2]], [[VAL]]
+; CHECK-NEXT:    br i1 [[CMP_VAL_2]], label %[[COMMON_RET_LOOPEXIT_UNR_LCSSA_LOOPEXIT_SPLIT_LOOP_EXIT12:.*]], label %[[FOR_COND_2:.*]]
+; CHECK:       [[FOR_COND_2]]:
+; CHECK-NEXT:    [[PTR_DEC_2:%.*]] = getelementptr inbounds i8, ptr [[PTR_DEC3]], i64 -3
+; CHECK-NEXT:    [[LOAD_3:%.*]] = load i8, ptr [[PTR_DEC_2]], align 1
+; CHECK-NEXT:    [[CMP_VAL_3:%.*]] = icmp eq i8 [[LOAD_3]], [[VAL]]
+; CHECK-NEXT:    br i1 [[CMP_VAL_3]], label %[[COMMON_RET_LOOPEXIT_UNR_LCSSA_LOOPEXIT_SPLIT_LOOP_EXIT10:.*]], label %[[FOR_COND_3:.*]]
+; CHECK:       [[FOR_COND_3]]:
+; CHECK-NEXT:    [[PTR_DEC_3:%.*]] = getelementptr inbounds i8, ptr [[PTR_DEC3]], i64 -4
+; CHECK-NEXT:    [[CMP_END:%.*]] = icmp eq ptr [[PTR_DEC_3]], [[BEGIN]]
+; CHECK-NEXT:    br i1 [[CMP_END]], label %[[COMMON_RET]], label %[[FOR_BODY]]
+; CHECK:       [[FOR_BODY]]:
+; CHECK-NEXT:    [[PTR_DEC3]] = phi ptr [ [[PTR_DEC_3]], %[[FOR_COND_3]] ], [ [[PTR_DEC3_UNR]], %[[FOR_BODY_PROL_LOOPEXIT]] ]
+; CHECK-NEXT:    [[LOAD:%.*]] = load i8, ptr [[PTR_DEC3]], align 1
+; CHECK-NEXT:    [[CMP_VAL:%.*]] = icmp eq i8 [[LOAD]], [[VAL]]
+; CHECK-NEXT:    br i1 [[CMP_VAL]], label %[[COMMON_RET]], label %[[FOR_COND1]]
+; CHECK:       [[COMMON_RET_LOOPEXIT_UNR_LCSSA_LOOPEXIT_SPLIT_LOOP_EXIT10]]:
+; CHECK-NEXT:    [[PTR_DEC_2_LE:%.*]] = getelementptr inbounds i8, ptr [[PTR_DEC3]], i64 -3
+; CHECK-NEXT:    br label %[[COMMON_RET]]
+; CHECK:       [[COMMON_RET_LOOPEXIT_UNR_LCSSA_LOOPEXIT_SPLIT_LOOP_EXIT12]]:
+; CHECK-NEXT:    [[PTR_DEC_1_LE:%.*]] = getelemen...
[truncated]

@@ -0,0 +1,124 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
; RUN: opt -S -passes='loop(loop-rotate<header-duplication>)' %s | FileCheck %s --check-prefix=CHECK

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.

Missing the check-exit-count pass option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fixed now, thanks

@github-actions

github-actions Bot commented Mar 19, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 192501 tests passed
  • 4943 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Mar 19, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 132535 tests passed
  • 3021 tests skipped

✅ The build succeeded and all tests passed.

fhahn added 3 commits March 19, 2026 12:12
Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral
https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (~+0.15%) and 7zip faster in most cases
(~-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after 5f648c3 which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on llvm#162654.
@fhahn
fhahn force-pushed the loop-rotate-countable branch from 249ac02 to adc04f9 Compare March 19, 2026 12:46

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

LGTM

It's unfortunate that this rotation happens late, but I guess this is the best we can easily do.

@fhahn
fhahn requested a review from efriedma-quic March 19, 2026 16:32
@fhahn
fhahn enabled auto-merge (squash) March 20, 2026 09:55
@fhahn
fhahn merged commit 21f439f into llvm:main Mar 20, 2026
10 checks passed
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 20, 2026
…itability (#187483)

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral

https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (~+0.15%) and 7zip faster in most cases
(~-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after
llvm/llvm-project@5f648c3
which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on llvm/llvm-project#162654.

Co-authored-by:  Marek Sedláček <[email protected]>

PR: llvm/llvm-project#187483
@fhahn
fhahn deleted the loop-rotate-countable branch March 24, 2026 12:01
fhahn added a commit to fhahn/llvm-project that referenced this pull request Mar 28, 2026
…lvm#187483)

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral

https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (~+0.15%) and 7zip faster in most cases
(~-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after
llvm@5f648c3
which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on llvm#162654.

Co-authored-by:  Marek Sedláček <[email protected]>

PR: llvm#187483

(cherry-picked from 21f439f)
Aadarsh-Keshri pushed a commit to Aadarsh-Keshri/llvm-project that referenced this pull request Apr 1, 2026
…lvm#187483)

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral

https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (~+0.15%) and 7zip faster in most cases
(~-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after
llvm@5f648c3
which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on llvm#162654.

Co-authored-by:  Marek Sedláček <[email protected]>

PR: llvm#187483
@aeubanks

aeubanks commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

we've bisected a non-trivial size increase in -Os builds to this commit, is that expected?

@fhahn fhahn left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we've bisected a non-trivial size increase in -Os builds to this commit, is that expected?

We should still only rotate if the size heuristics allow it. So the increase from rotation itself should be minor, but there could be downstream effects of rotation enabling additional transformations, which do not properly account for size increase?

@evodius96

evodius96 commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

We've encountered what appears to be an infinite runtime with our downstream Arm compiler that we've bisected to this commit. It happens in SolidSands ACE test suite (stress/tcrc8itut.c), and unfortunately it'll be difficult to get a reproducer. As it's a stress test anyway, we can note if this is pathological or showing up in other places. Presently we time out.

@thurstond

thurstond commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

I've bisected a LoopVectorizer crash (perhaps pre-existing but unlocked after the loop rotation?) to this PR.

Minimized reproducer showing the crash at trunk: https://godbolt.org/z/soqMM53T8


Crashes with opt -O2:

; ModuleID = 'reduced.bc'
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-grtev4-linux-gnu"

; Function Attrs: cold noreturn nounwind memory(inaccessiblemem: write)
declare void @llvm.ubsantrap(i8 immarg) #0

; Function Attrs: nocallback nocreateundeforpoison nofree nosync nounwind speculatable willreturn memory(none)
declare { i64, i1 } @llvm.sadd.with.overflow.i64(i64, i64) #1

define i1 @pch_swap(i64 %.pre) #2 {
entry:
  br label %for.cond

trap:                                             ; preds = %cont38, %for.body
  call void @llvm.ubsantrap(i8 0)
  unreachable

for.cond:                                         ; preds = %cont39, %entry
  %i.0 = phi i64 [ %.pre, %entry ], [ %dividends, %cont39 ]
  %n.0 = phi i64 [ 0, %entry ], [ %5, %cont39 ]
  %cmp23.not = icmp sgt i64 %i.0, %.pre
  br i1 %cmp23.not, label %for.end, label %for.body

for.body:                                         ; preds = %for.cond
  %arrayidx37 = getelementptr [8 x i8], ptr null, i64 %n.0
  store i64 0, ptr %arrayidx37, align 8
  %company = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %i.0, i64 1)
  %dividends = extractvalue { i64, i1 } %company, 0
  %buybacks = extractvalue { i64, i1 } %company, 1
  br i1 %buybacks, label %trap, label %cont38

cont38:                                           ; preds = %for.body
  %3 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %n.0, i64 1)
  %4 = extractvalue { i64, i1 } %3, 1
  br i1 %4, label %trap, label %cont39

cont39:                                           ; preds = %cont38
  %5 = extractvalue { i64, i1 } %3, 0
  br label %for.cond

for.end:                                          ; preds = %for.cond
  ret i1 false
}

attributes #0 = { cold noreturn nounwind memory(inaccessiblemem: write) }
attributes #1 = { nocallback nocreateundeforpoison nofree nosync nounwind speculatable willreturn memory(none) }
attributes #2 = { "target-features"="+avx" }

Symbolized crash at this PR:

opt: llvm-projectK/llvm/include/llvm/IR/User.h:213: void llvm::User::setOperand(unsigned int, Value *): Assertion `i < NumUserOperands && "setOperand() out of range!"' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
Stack dump:
0.	Program arguments: bin/opt -O2 -disable-output reduced.ll
1.	Running pass "function<eager-inv>(drop-unnecessary-assumes,float2int,lower-constant-intrinsics,loop(loop-rotate<header-duplication;no-prepare-for-lto;check-exit-count>,loop-deletion),loop-distribute,inject-tli-mappings,loop-vectorize<no-interleave-forced-only;no-vectorize-forced-only;>,drop-unnecessary-assumes,infer-alignment,loop-load-elim,instcombine<max-iterations=1;no-verify-fixpoint>,simplifycfg<bonus-inst-threshold=1;forward-switch-cond;switch-range-to-icmp;switch-to-arithmetic;switch-to-lookup;no-keep-loops;hoist-common-insts;no-hoist-loads-stores-with-cond-faulting;sink-common-insts;speculate-blocks;simplify-cond-branch;no-speculate-unpredictables>,slp-vectorizer,vector-combine,instcombine<max-iterations=1;no-verify-fixpoint>,loop-unroll<O2>,transform-warning,sroa<preserve-cfg>,infer-alignment,instcombine<max-iterations=1;no-verify-fixpoint>,loop-mssa(licm<allowspeculation>),alignment-from-assumptions,loop-sink,instsimplify,div-rem-pairs,tailcallelim,simplifycfg<bonus-inst-threshold=1;no-forward-switch-cond;switch-range-to-icmp;switch-to-arithmetic;no-switch-to-lookup;keep-loops;no-hoist-common-insts;hoist-loads-stores-with-cond-faulting;no-sink-common-insts;speculate-blocks;simplify-cond-branch;speculate-unpredictables>)" on module "reduced.ll"
2.	Running pass "loop-vectorize<no-interleave-forced-only;no-vectorize-forced-only;>" on function "pch_swap"
 #0 0x00005650515e9b4d llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) llvm-projectK/llvm/lib/Support/Unix/Signals.inc:880:11
 #1 0x00005650515ea07b PrintStackTraceSignalHandler(void*) llvm-projectK/llvm/lib/Support/Unix/Signals.inc:962:1
 #2 0x00005650515e7f14 llvm::sys::RunSignalHandlers() llvm-projectK/llvm/lib/Support/Signals.cpp:108:5
 #3 0x00005650515ea7a9 SignalHandler(int, siginfo_t*, void*) llvm-projectK/llvm/lib/Support/Unix/Signals.inc:448:38
 #4 0x00007f1dafe40a70 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x40a70)
 #5 0x00007f1dafe973dc __pthread_kill_implementation ./nptl/pthread_kill.c:44:76
 #6 0x00007f1dafe40942 raise ./signal/../sysdeps/posix/raise.c:27:6
 #7 0x00007f1dafe284ac abort ./stdlib/abort.c:85:3
 #8 0x00007f1dafe28420 __assert_perror_fail ./assert/assert-perr.c:31:1
 #9 0x000056504b2b2b51 llvm::User::setOperand(unsigned int, llvm::Value*) llvm-projectK/llvm/include/llvm/IR/User.h:0:5
#10 0x000056504ea3f981 scalarizeInstruction(llvm::Instruction const*, llvm::VPReplicateRecipe*, llvm::VPLane const&, llvm::VPTransformState&) llvm-projectK/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp:3279:22
#11 0x000056504ea3f410 llvm::VPReplicateRecipe::execute(llvm::VPTransformState&) llvm-projectK/llvm/lib/Transforms/Vectorize/VPlanRecipes.cpp:3312:5
#12 0x000056504e9ab8db llvm::VPBasicBlock::executeRecipes(llvm::VPTransformState*, llvm::BasicBlock*) llvm-projectK/llvm/lib/Transforms/Vectorize/VPlan.cpp:557:29
#13 0x000056504e9abc19 llvm::VPBasicBlock::execute(llvm::VPTransformState*) llvm-projectK/llvm/lib/Transforms/Vectorize/VPlan.cpp:537:3
#14 0x000056504e9aec5e llvm::VPlan::execute(llvm::VPTransformState*) llvm-projectK/llvm/lib/Transforms/Vectorize/VPlan.cpp:965:27
#15 0x000056504e6bf646 llvm::LoopVectorizationPlanner::executePlan(llvm::ElementCount, unsigned int, llvm::VPlan&, llvm::InnerLoopVectorizer&, llvm::DominatorTree*, bool) llvm-projectK/llvm/lib/Transforms/Vectorize/LoopVectorize.cpp:7557:58
#16 0x000056504e6c8524 llvm::LoopVectorizePass::processLoop(llvm::Loop*) llvm-projectK/llvm/lib/Transforms/Vectorize/LoopVectorize.cpp:9911:5
#17 0x000056504e6ccfcb llvm::LoopVectorizePass::runImpl(llvm::Function&) llvm-projectK/llvm/lib/Transforms/Vectorize/LoopVectorize.cpp:9964:30
#18 0x000056504e6cd3db llvm::LoopVectorizePass::run(llvm::Function&, llvm::AnalysisManager<llvm::Function>&) llvm-projectK/llvm/lib/Transforms/Vectorize/LoopVectorize.cpp:10002:32
#19 0x000056504de263d4 llvm::detail::PassModel<llvm::Function, llvm::LoopVectorizePass, llvm::AnalysisManager<llvm::Function>>::run(llvm::Function&, llvm::AnalysisManager<llvm::Function>&) llvm-projectK/llvm/include/llvm/IR/PassManagerInternal.h:91:17
#20 0x000056505127b72d llvm::PassManager<llvm::Function, llvm::AnalysisManager<llvm::Function>>::run(llvm::Function&, llvm::AnalysisManager<llvm::Function>&) llvm-projectK/llvm/include/llvm/IR/PassManagerImpl.h:80:5
#21 0x000056504b798154 llvm::detail::PassModel<llvm::Function, llvm::PassManager<llvm::Function, llvm::AnalysisManager<llvm::Function>>, llvm::AnalysisManager<llvm::Function>>::run(llvm::Function&, llvm::AnalysisManager<llvm::Function>&) llvm-projectK/llvm/include/llvm/IR/PassManagerInternal.h:91:17
#22 0x000056505127a426 llvm::ModuleToFunctionPassAdaptor::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) llvm-projectK/llvm/lib/IR/PassManager.cpp:127:38
#23 0x000056504ae8c314 llvm::detail::PassModel<llvm::Module, llvm::ModuleToFunctionPassAdaptor, llvm::AnalysisManager<llvm::Module>>::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) llvm-projectK/llvm/include/llvm/IR/PassManagerInternal.h:91:17
#24 0x000056505127a9cd llvm::PassManager<llvm::Module, llvm::AnalysisManager<llvm::Module>>::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) llvm-projectK/llvm/include/llvm/IR/PassManagerImpl.h:80:5
#25 0x000056504ae6416e llvm::runPassPipeline(llvm::StringRef, llvm::Module&, llvm::TargetMachine*, llvm::TargetLibraryInfoImpl*, llvm::ToolOutputFile*, llvm::ToolOutputFile*, llvm::ToolOutputFile*, llvm::StringRef, llvm::ArrayRef<llvm::PassPlugin>, llvm::ArrayRef<std::function<void (llvm::PassBuilder&)>>, llvm::opt_tool::OutputKind, llvm::opt_tool::VerifierKind, bool, bool, bool, bool, bool, bool, bool, bool) llvm-projectK/llvm/tools/opt/NewPMDriver.cpp:574:3
#26 0x000056504ae3d68b optMain llvm-projectK/llvm/tools/opt/optdriver.cpp:751:9
#27 0x000056504ae3b471 main llvm-projectK/llvm/tools/opt/opt.cpp:27:35
#28 0x00007f1dafe29f75 __libc_start_call_main ./csu/../sysdeps/nptl/libc_start_call_main.h:74:3
#29 0x00007f1dafe2a027 call_init ./csu/../csu/libc-start.c:128:20
#30 0x00007f1dafe2a027 __libc_start_main ./csu/../csu/libc-start.c:347:5
#31 0x000056504ae3b351 _start (bin/opt+0xead351)

The setOperand() assertion is failing on an extractvalue instruction. ExtractValueInst only has one operand according to getNumOperands() (the subsequent "operands" - indices - are handled separately), thus it has a bad time with:

  // Replace the operands of the cloned instructions with their scalar
  // equivalents in the new loop.
  for (const auto &I : enumerate(RepRecipe->operands())) {
    auto InputLane = Lane;
    VPValue *Operand = I.value();
    if (vputils::isSingleScalar(Operand))
      InputLane = VPLane::getFirstLane();
    Cloned->setOperand(I.index(), State.get(Operand, InputLane)); // CRASH HERE
  }

@thurstond

Copy link
Copy Markdown
Contributor

I've bisected a LoopVectorizer crash (perhaps pre-existing but unlocked after the loop rotation?) to this PR.
...

I reduced my test case further (https://godbolt.org/z/qGx86MnT7) into an input that fails even prior to this PR. I'll bisect further to find the true root cause.

markrvmurray pushed a commit to markrvmurray/llvm-mc6809 that referenced this pull request Jun 14, 2026
…#187483)

Most loop transformations, like unrolling and vectorization, expect the
latch branch to be countable. Allow rotation, if it turns the latch from
uncountable to countable.

This use SCEV to check for countable exits, if CheckExitCount set.
Currently it is not set for the LPM1 run (where SCEV is not used by
other passes), only in LPM.

With that compile-time impact is mostly neutral

https://llvm-compile-time-tracker.com/compare.php?from=eba342d0ba930a404a026c80aada51c43974f0db&to=2e676337b45fae63ce9498116d8e6e43772363c5&stat=instructions:u

ClamAV is consistently slower (~+0.15%) and 7zip faster in most cases
(~-0.13%)

Across a large test set based on C/C++ workloads, this rotates ~0.8%
more loops with ~2.68M rotated loops.

For the test set, ~2.7% more loops are runtime-unrolled and +6.36% more
early exit loops vectorized on ARM64 macOS.

This fixes a regression where std::ranges::find_last loops stopped
being runtime-unrolled after
llvm/llvm-project@5f648c3
which changed the loop
structure so we stopped rotating.

https://clang.godbolt.org/z/6baeE1av6

Based on llvm/llvm-project#162654.

Co-authored-by:  Marek Sedláček <[email protected]>

PR: llvm/llvm-project#187483
delcypher added a commit to delcypher/apple-llvm-project that referenced this pull request Jun 24, 2026
…s-structs(|-no-bringup-missing-checks.c) failures

The CHECK lines for the `access_struct_1_checks_needed` function in
these two tests expected an un-rotated loop shape. After cherry-picking
`b272c36f15b0` (*[LoopRotate] Use SCEV exit counts to improve rotation
profitability (llvm#187483)*), this loop now gets rotated, and the previous
CHECK lines no longer match.

This was verified by capturing the canonical `default<O2>` pipeline
string from `opt --print-pipeline-passes` and re-running it after
flipping the LPM `loop-rotate<...;check-exit-count>` parameter to
`no-check-exit-count`. With the new behavior disabled the un-rotated
shape comes back; with it enabled (the post-cherry-pick default) the
rotated shape is produced.

CHECK lines were regenerated with `llvm/utils/update_cc_test_checks.py`
using the in-tree clang, after picking up `c35b15eef009` (*[UTC] Fix
bounds safety default checks in update_cc_test_checks.py*) so the
script no longer clobbers tests that explicitly pass
`-fno-bounds-safety-bringup-missing-checks=all`.

Assisted-by: Claude Code

rdar://179378974
devincoughlin pushed a commit to swiftlang/llvm-project that referenced this pull request Jun 25, 2026
…s-structs(|-no-bringup-missing-checks.c) failures

The CHECK lines for the `access_struct_1_checks_needed` function in
these two tests expected an un-rotated loop shape. After cherry-picking
`b272c36f15b0` (*[LoopRotate] Use SCEV exit counts to improve rotation
profitability (llvm#187483)*), this loop now gets rotated, and the previous
CHECK lines no longer match.

This was verified by capturing the canonical `default<O2>` pipeline
string from `opt --print-pipeline-passes` and re-running it after
flipping the LPM `loop-rotate<...;check-exit-count>` parameter to
`no-check-exit-count`. With the new behavior disabled the un-rotated
shape comes back; with it enabled (the post-cherry-pick default) the
rotated shape is produced.

CHECK lines were regenerated with `llvm/utils/update_cc_test_checks.py`
using the in-tree clang, after picking up `c35b15eef009` (*[UTC] Fix
bounds safety default checks in update_cc_test_checks.py*) so the
script no longer clobbers tests that explicitly pass
`-fno-bounds-safety-bringup-missing-checks=all`.

Assisted-by: Claude Code

rdar://179378974
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants