[llvm] [LoopInterchange] Do not interchange guarded imperfect loop nests (PR #201504)
Madhur Amilkanthwar via llvm-commits
llvm-commits at lists.llvm.org
Thu Jun 4 08:07:48 PDT 2026
https://github.com/madhur13490 updated https://github.com/llvm/llvm-project/pull/201504
>From 672f79c5aff21af3ca9f934f0b533797a2d83bcb Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Wed, 3 Jun 2026 06:24:20 -0700
Subject: [PATCH 1/3] [LoopInterchange] Do not interchange guarded imperfect
loop nests
When the outer-loop header conditionally branches to the outer latch, that
branch guards the inner loop, so the inner loop runs only on a subset of the
outer iterations. Interchanging such a nest moves the inner loop outside the
guard and runs it on every outer iteration, including the guarded-off ones.
That is incorrect when the inner loop relies on the guard to terminate (e.g.
an eq/ne exit whose trip count is degenerate once the guard is false): the
extra runs do not terminate, so the program hangs at run time.
Reject these guarded, imperfect nests in tightlyNested().
Most importantly, three existing tests
(currentLimitation.ll, loop-interchange-optimization-remarks.ll,
lcssa-preheader.ll) were going through this unsafe transform;
these tests would no longer interchange with this patch.
I verified that they were hanging after illegal loop interchange with `lli`:
the un-interchanged function returns but the interchanged one spins for a
degenerate input (interchange_01(0, 2), test02(0, 2), lcssa_08(1, 0)).
Fixes #201273
---
.../lib/Transforms/Scalar/LoopInterchange.cpp | 21 ++++++
.../LoopInterchange/currentLimitation.ll | 19 ++++--
.../LoopInterchange/lcssa-preheader.ll | 65 +++++++++---------
.../loop-interchange-optimization-remarks.ll | 23 ++++---
.../pr201273-guarded-inner-loop.ll | 67 +++++++++++++++++++
5 files changed, 150 insertions(+), 45 deletions(-)
create mode 100644 llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
diff --git a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
index 9d424ae13e28a..ba0a3f14019c0 100644
--- a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
@@ -832,6 +832,27 @@ bool LoopInterchangeLegality::tightlyNested(Loop *OuterLoop, Loop *InnerLoop) {
Succ != OuterLoopLatch)
return false;
+ // Reject nests where the outer-loop header conditionally branches to the
+ // outer latch. Such a branch guards the inner loop, so it runs only on a
+ // subset of the outer iterations. If interchanged, the inner loop would move
+ // outside the guard and run on every outer iteration, including the
+ // guarded-off ones. That is illegal when the inner loop relies on the guard
+ // to terminate.
+ //
+ // TODO: Bailing out is conservative; such a nest can be interchanged when
+ // the guard is preserved by the transform. If the guard condition is
+ // invariant across the interchanged pair, keep it wrapping the interchanged
+ // nest so the inner loop is skipped for the outer iterations the guard
+ // excludes, instead of routing the guard edge into the inner latch as
+ // adjustLoopBranches() does today. Only bail when the guard depends on the
+ // interchanged induction variables or the inner loop cannot be proven to
+ // run zero times when the guard is false. See llvm/llvm-project#201273.
+ if (OuterLoopHeader->getTerminator()->getNumSuccessors() > 1 &&
+ is_contained(successors(OuterLoopHeader), OuterLoopLatch)) {
+ LLVM_DEBUG(dbgs() << "Outer loop header guards the inner loop\n");
+ return false;
+ }
+
LLVM_DEBUG(dbgs() << "Checking instructions in Loop header and Loop latch\n");
// The inner loop reduction pattern requires storing the LCSSA PHI in
diff --git a/llvm/test/Transforms/LoopInterchange/currentLimitation.ll b/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
index 66e509d03d944..8af9a4d0ad89d 100644
--- a/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
+++ b/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
@@ -14,16 +14,25 @@ target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
@C = common global [100 x [100 x i64]] zeroinitializer
;;--------------------------------------Test case 01------------------------------------
-;; This loop can be interchanged with -da-disable-delinearization-checks, otherwise it cannot
-;; be interchanged due to dependence.
+;; Without -da-disable-delinearization-checks this is not interchanged due to
+;; dependence. Even with that flag it must not be interchanged: it is a guarded,
+;; imperfect nest (see below).
;; for(int i=0;i<N-1;i++)
-;; for(int j=1;j<N-1;j++)
-;; A[j+1][i+1] = A[j+1][i+1] + k;
+;; if(N-1>1) // guard: inner loop skipped when N<=2
+;; for(int j=1;j<N-1;j++)
+;; A[j+1][i+1] = A[j+1][i+1] + k;
; CHECK: Name: Dependence
; CHECK-NEXT: Function: interchange_01
-; DELIN: Name: Interchanged
+; %for.cond1.preheader guards the inner loop: it branches to the inner loop
+; (%for.body4) or straight to the outer latch (%for.cond.loopexit) based on
+; %cmp324 = (N-1 > 1). The inner loop exits on (inner IV == N-2), so it only
+; terminates because the guard stops it from running when N <= 2. Interchanging
+; would move the inner loop outside the guard and run it on every outer
+; iteration; with N == 2 (reachable, since the nest is entered for N > 1) the
+; inner exit is never taken and it loops forever.
+; DELIN: Name: NotTightlyNested
; DELIN-NEXT: Function: interchange_01
define void @interchange_01(i32 %k, i32 %N) {
entry:
diff --git a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
index 66078a857fe3c..42a5fd1c6000e 100644
--- a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
+++ b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
@@ -8,13 +8,21 @@ target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
; int temp[16][16];
; int res[16][16];
; for(int i = 0; i < n; i++) {
-; for(int j = 0; j < m; j++)
-; res[j][i] = temp[j][i];
+; if (m > 0) // guard: inner loop skipped when m <= 0
+; for(int j = 0; j < m; j++)
+; res[j][i] = temp[j][i];
; }
; }
-;; This loop can be interchanged with -da-disable-delinearization-checks, otherwise it cannot
-;; be interchanged due to dependence.
+;; Without -da-disable-delinearization-checks this is not interchanged due to
+;; dependence. Even with that flag it must not be interchanged: %outer.header
+;; guards the inner loop by branching to it or to the outer latch based on
+;; %cmp222 = (m > 0). The inner loop exits on (iv.next != m) with a nuw IV, so
+;; it only terminates when m > 0. Interchanging would run the inner loop on
+;; every outer iteration; with m == 0 (and n > 0) the exit is never taken and
+;; it loops forever. Verified with lli: lcssa_08(1, 0) returns before
+;; interchange but hangs after it, so the CHECK-DELIN run below must also leave
+;; it un-interchanged.
define void @lcssa_08(i32 %n, i32 %m) {;
; CHECK-LABEL: define void @lcssa_08(
; CHECK-SAME: i32 [[N:%.*]], i32 [[M:%.*]]) {
@@ -59,39 +67,32 @@ define void @lcssa_08(i32 %n, i32 %m) {;
; CHECK-DELIN-NEXT: [[TEMP:%.*]] = alloca [16 x [16 x i32]], align 4
; CHECK-DELIN-NEXT: [[RES:%.*]] = alloca [16 x [16 x i32]], align 4
; CHECK-DELIN-NEXT: [[CMP24:%.*]] = icmp sgt i32 [[N]], 0
-; CHECK-DELIN-NEXT: br i1 [[CMP24]], label %[[INNER_PREHEADER:.*]], label %[[FOR_COND_CLEANUP:.*]]
-; CHECK-DELIN: [[OUTER_PREHEADER:.*]]:
+; CHECK-DELIN-NEXT: br i1 [[CMP24]], label %[[OUTER_PREHEADER:.*]], label %[[FOR_COND_CLEANUP:.*]]
+; CHECK-DELIN: [[OUTER_PREHEADER]]:
+; CHECK-DELIN-NEXT: [[WIDE_TRIP_COUNT29:%.*]] = zext i32 [[N]] to i64
; CHECK-DELIN-NEXT: br label %[[OUTER_HEADER:.*]]
; CHECK-DELIN: [[OUTER_HEADER]]:
; CHECK-DELIN-NEXT: [[INDVARS_IV27:%.*]] = phi i64 [ 0, %[[OUTER_PREHEADER]] ], [ [[INDVARS_IV_NEXT28:%.*]], %[[OUTER_LATCH:.*]] ]
; CHECK-DELIN-NEXT: [[CMP222:%.*]] = icmp sgt i32 [[M]], 0
-; CHECK-DELIN-NEXT: [[WIDE_TRIP_COUNT:%.*]] = zext i32 [[M]] to i64
-; CHECK-DELIN-NEXT: br i1 [[CMP222]], label %[[INNER_FOR_BODY_SPLIT1:.*]], label %[[INNER_FOR_BODY_SPLIT:.*]]
+; CHECK-DELIN-NEXT: br i1 [[CMP222]], label %[[INNER_PREHEADER:.*]], label %[[OUTER_LATCH]]
; CHECK-DELIN: [[INNER_PREHEADER]]:
-; CHECK-DELIN-NEXT: [[WIDE_TRIP_COUNT29:%.*]] = zext i32 [[N]] to i64
+; CHECK-DELIN-NEXT: [[WIDE_TRIP_COUNT:%.*]] = zext i32 [[M]] to i64
; CHECK-DELIN-NEXT: br label %[[INNER_FOR_BODY:.*]]
; CHECK-DELIN: [[INNER_FOR_BODY]]:
-; CHECK-DELIN-NEXT: [[INDVARS_IV:%.*]] = phi i64 [ 0, %[[INNER_PREHEADER]] ], [ [[TMP1:%.*]], %[[INNER_FOR_BODY_SPLIT]] ]
-; CHECK-DELIN-NEXT: br label %[[OUTER_PREHEADER]]
-; CHECK-DELIN: [[INNER_FOR_BODY_SPLIT1]]:
+; CHECK-DELIN-NEXT: [[INDVARS_IV:%.*]] = phi i64 [ 0, %[[INNER_PREHEADER]] ], [ [[INDVARS_IV_NEXT:%.*]], %[[INNER_FOR_BODY]] ]
; CHECK-DELIN-NEXT: [[ARRAYIDX6:%.*]] = getelementptr inbounds [16 x [16 x i32]], ptr [[TEMP]], i64 0, i64 [[INDVARS_IV]], i64 [[INDVARS_IV27]]
; CHECK-DELIN-NEXT: [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX6]], align 4
; CHECK-DELIN-NEXT: [[ARRAYIDX8:%.*]] = getelementptr inbounds [16 x [16 x i32]], ptr [[RES]], i64 0, i64 [[INDVARS_IV]], i64 [[INDVARS_IV27]]
; CHECK-DELIN-NEXT: store i32 [[TMP0]], ptr [[ARRAYIDX8]], align 4
-; CHECK-DELIN-NEXT: [[INDVARS_IV_NEXT:%.*]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-DELIN-NEXT: [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
; CHECK-DELIN-NEXT: [[EXITCOND:%.*]] = icmp ne i64 [[INDVARS_IV_NEXT]], [[WIDE_TRIP_COUNT]]
-; CHECK-DELIN-NEXT: br label %[[INNER_CRIT_EDGE:.*]]
-; CHECK-DELIN: [[INNER_FOR_BODY_SPLIT]]:
-; CHECK-DELIN-NEXT: [[WIDE_TRIP_COUNT_LCSSA:%.*]] = phi i64 [ [[WIDE_TRIP_COUNT]], %[[OUTER_LATCH]] ], [ [[WIDE_TRIP_COUNT]], %[[OUTER_HEADER]] ]
-; CHECK-DELIN-NEXT: [[TMP1]] = add nuw nsw i64 [[INDVARS_IV]], 1
-; CHECK-DELIN-NEXT: [[TMP2:%.*]] = icmp ne i64 [[TMP1]], [[WIDE_TRIP_COUNT_LCSSA]]
-; CHECK-DELIN-NEXT: br i1 [[TMP2]], label %[[INNER_FOR_BODY]], label %[[OUTER_CRIT_EDGE:.*]]
+; CHECK-DELIN-NEXT: br i1 [[EXITCOND]], label %[[INNER_FOR_BODY]], label %[[INNER_CRIT_EDGE:.*]]
; CHECK-DELIN: [[INNER_CRIT_EDGE]]:
; CHECK-DELIN-NEXT: br label %[[OUTER_LATCH]]
; CHECK-DELIN: [[OUTER_LATCH]]:
; CHECK-DELIN-NEXT: [[INDVARS_IV_NEXT28]] = add nuw nsw i64 [[INDVARS_IV27]], 1
; CHECK-DELIN-NEXT: [[EXITCOND30:%.*]] = icmp ne i64 [[INDVARS_IV_NEXT28]], [[WIDE_TRIP_COUNT29]]
-; CHECK-DELIN-NEXT: br i1 [[EXITCOND30]], label %[[OUTER_HEADER]], label %[[INNER_FOR_BODY_SPLIT]]
+; CHECK-DELIN-NEXT: br i1 [[EXITCOND30]], label %[[OUTER_HEADER]], label %[[OUTER_CRIT_EDGE:.*]]
; CHECK-DELIN: [[OUTER_CRIT_EDGE]]:
; CHECK-DELIN-NEXT: br label %[[FOR_COND_CLEANUP]]
; CHECK-DELIN: [[FOR_COND_CLEANUP]]:
@@ -156,21 +157,21 @@ define void @test2(i32 %N) {
; CHECK-NEXT: [[BB:.*]]:
; CHECK-NEXT: br label %[[OUTER_HEADER:.*]]
; CHECK: [[OUTER_HEADER]]:
-; CHECK-NEXT: [[OUTER_IV:%.*]] = phi i64 [ 0, %[[BB]] ], [ [[OUTER_IV_NEXT:%.*]], %[[EXIT:.*]] ]
+; CHECK-NEXT: [[OUTER_IV:%.*]] = phi i64 [ 0, %[[BB]] ], [ [[OUTER_IV_NEXT:%.*]], %[[OUTER_LATCH:.*]] ]
; CHECK-NEXT: [[N_EXT:%.*]] = sext i32 [[N]] to i64
; CHECK-NEXT: br label %[[INNER:.*]]
; CHECK: [[INNER]]:
-; CHECK-NEXT: [[INNER_IV:%.*]] = phi i64 [ 0, %[[OUTER_HEADER]] ], [ [[TMP0:%.*]], %[[INNER]] ]
-; CHECK-NEXT: [[TMP8:%.*]] = getelementptr inbounds [2 x i16], ptr @global, i64 [[INNER_IV]], i64 [[OUTER_IV]]
-; CHECK-NEXT: store i16 0, ptr [[TMP8]], align 2
-; CHECK-NEXT: [[TMP0]] = add nsw i64 [[INNER_IV]], 1
-; CHECK-NEXT: [[TMP1:%.*]] = icmp ne i64 [[TMP0]], [[N_EXT]]
-; CHECK-NEXT: br i1 [[TMP1]], label %[[INNER]], label %[[EXIT]]
-; CHECK: [[EXIT]]:
+; CHECK-NEXT: [[INNER_IV:%.*]] = phi i64 [ 0, %[[OUTER_HEADER]] ], [ [[INNER_IV_NEXT:%.*]], %[[INNER]] ]
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr inbounds [2 x i16], ptr @global, i64 [[INNER_IV]], i64 [[OUTER_IV]]
+; CHECK-NEXT: store i16 0, ptr [[GEP]], align 2
+; CHECK-NEXT: [[INNER_IV_NEXT]] = add nsw i64 [[INNER_IV]], 1
+; CHECK-NEXT: [[C_1:%.*]] = icmp ne i64 [[INNER_IV_NEXT]], [[N_EXT]]
+; CHECK-NEXT: br i1 [[C_1]], label %[[INNER]], label %[[OUTER_LATCH]]
+; CHECK: [[OUTER_LATCH]]:
; CHECK-NEXT: [[OUTER_IV_NEXT]] = add nsw i64 [[OUTER_IV]], 1
; CHECK-NEXT: [[C_2:%.*]] = icmp ne i64 [[OUTER_IV]], [[N_EXT]]
-; CHECK-NEXT: br i1 [[C_2]], label %[[OUTER_HEADER]], label %[[EXIT1:.*]]
-; CHECK: [[EXIT1]]:
+; CHECK-NEXT: br i1 [[C_2]], label %[[OUTER_HEADER]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
; CHECK-NEXT: ret void
;
; CHECK-DELIN-LABEL: define void @test2(
@@ -189,8 +190,8 @@ define void @test2(i32 %N) {
; CHECK-DELIN-NEXT: [[INNER_IV:%.*]] = phi i64 [ [[TMP0:%.*]], %[[INNER_SPLIT:.*]] ], [ 0, %[[INNER_PREHEADER]] ]
; CHECK-DELIN-NEXT: br label %[[OUTER_HEADER_PREHEADER]]
; CHECK-DELIN: [[INNER_SPLIT1]]:
-; CHECK-DELIN-NEXT: [[TMP8:%.*]] = getelementptr inbounds [2 x i16], ptr @global, i64 [[INNER_IV]], i64 [[OUTER_IV]]
-; CHECK-DELIN-NEXT: store i16 0, ptr [[TMP8]], align 2
+; CHECK-DELIN-NEXT: [[GEP:%.*]] = getelementptr inbounds [2 x i16], ptr @global, i64 [[INNER_IV]], i64 [[OUTER_IV]]
+; CHECK-DELIN-NEXT: store i16 0, ptr [[GEP]], align 2
; CHECK-DELIN-NEXT: [[INNER_IV_NEXT:%.*]] = add nsw i64 [[INNER_IV]], 1
; CHECK-DELIN-NEXT: [[C_1:%.*]] = icmp ne i64 [[INNER_IV_NEXT]], [[N_EXT]]
; CHECK-DELIN-NEXT: br label %[[OUTER_LATCH]]
diff --git a/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll b/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
index 22b6efee6e11e..5c41bb9e84c92 100644
--- a/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
+++ b/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
@@ -92,12 +92,12 @@ for.end19:
; DELIN-NEXT: ...
;;--------------------------------------Test case 02------------------------------------
-;; [FIXME] This loop though valid is currently not interchanged due to the
-;; limitation that we cannot split the inner loop latch due to multiple use of inner induction
-;; variable.(used to increment the loop counter and to access A[j+1][i+1]
+;; A guarded, imperfect nest that must not be interchanged; see the
+;; explanation above the DELIN checks below.
;; for(int i=0;i<N-1;i++)
-;; for(int j=1;j<N-1;j++)
-;; A[j+1][i+1] = A[j+1][i+1] + k;
+;; if(N-1>1) // guard: inner loop skipped when N<=2
+;; for(int j=1;j<N-1;j++)
+;; A[j+1][i+1] = A[j+1][i+1] + k;
define void @test02(i32 %k, i32 %N) {
entry:
@@ -159,12 +159,19 @@ define void @test02(i32 %k, i32 %N) {
; DELIN-NEXT: - String: Computed dependence info, invoking the transform.
; DELIN-NEXT: ...
-; DELIN: --- !Passed
+; %for.cond1.preheader guards the inner loop: it branches to the inner loop
+; (%for.body4) or straight to the outer latch (%for.cond.loopexit) based on
+; %cmp324 = (N-1 > 1). The inner loop exits on (inner IV == N-2), so it only
+; terminates because the guard stops it from running when N <= 2. Interchanging
+; would move the inner loop outside the guard and run it on every outer
+; iteration; with N == 2 (reachable, since the nest is entered for N > 1) the
+; inner exit is never taken and it loops forever.
+; DELIN: --- !Missed
; DELIN-NEXT: Pass: loop-interchange
-; DELIN-NEXT: Name: Interchanged
+; DELIN-NEXT: Name: NotTightlyNested
; DELIN-NEXT: Function: test02
; DELIN-NEXT: Args:
-; DELIN-NEXT: - String: Loop interchanged with enclosing loop.
+; DELIN-NEXT: - String: Cannot interchange loops because they are not tightly nested.
; DELIN-NEXT: ...
;;-----------------------------------Test case 03-------------------------------
diff --git a/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll b/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
new file mode 100644
index 0000000000000..2805e7b8a42a7
--- /dev/null
+++ b/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
@@ -0,0 +1,67 @@
+; RUN: opt < %s -passes=loop-interchange -cache-line-size=64 \
+; RUN: -pass-remarks-missed='loop-interchange' -disable-output \
+; RUN: -pass-remarks-output=%t
+; RUN: FileCheck -input-file=%t %s
+
+; The middle loop header (%for.j) guards the inner loop %for.k: the inner loop
+; only runs when %j != 0, and its exit condition (%k.next == %j) is only
+; well-defined under that guard. Hoisting %for.k out of the guard would make it
+; spin when %j == 0, so the pass must not interchange this nest.
+
+; CHECK: --- !Missed
+; CHECK-NEXT: Pass: loop-interchange
+; CHECK-NEXT: Name: NotTightlyNested
+; CHECK-NEXT: Function: main
+
+ at x = global [3 x [3 x [3 x i32]]] zeroinitializer
+ at w = global [3 x [3 x [3 x i32]]] zeroinitializer
+ at y = global [3 x [3 x [3 x i32]]] zeroinitializer
+
+define i32 @main() {
+entry:
+ br label %for.i
+
+for.i:
+ %j = phi i32 [ %j.next, %for.i.inc ], [ 0, %entry ]
+ %j.is.zero = icmp eq i32 %j, 0
+ %xbase = getelementptr [9 x i32], ptr @x, i32 %j
+ %wbase = getelementptr [9 x i32], ptr @w, i32 %j
+ %ybase = getelementptr [3 x i32], ptr @y, i32 %j
+ br label %for.j
+
+for.j:
+ %i = phi i32 [ %i.next, %for.j.inc ], [ 0, %for.i ]
+ br i1 %j.is.zero, label %for.j.inc, label %for.k.ph
+
+for.k.ph:
+ %xp = getelementptr i32, ptr %xbase, i32 %i
+ %wp = getelementptr i32, ptr %wbase, i32 %i
+ %yp = getelementptr [9 x i32], ptr %ybase, i32 %i
+ br label %for.k
+
+for.k:
+ %k = phi i32 [ 0, %for.k.ph ], [ %k.next, %for.k ]
+ %xk = getelementptr [3 x i32], ptr %xp, i32 %k
+ %xv = load i32, ptr %xk, align 4
+ %wk = getelementptr [3 x i32], ptr %wp, i32 %k
+ %wv = load i32, ptr %wk, align 4
+ %add = add i32 %xv, %wv
+ %yk = getelementptr i32, ptr %yp, i32 %k
+ store i32 %add, ptr %yk, align 4
+ %k.next = add i32 %k, 1
+ %k.done = icmp eq i32 %k.next, %j
+ br i1 %k.done, label %for.j.inc, label %for.k
+
+for.j.inc:
+ %i.next = add i32 %i, 1
+ %i.done = icmp eq i32 %i, 0
+ br i1 %i.done, label %for.j, label %for.i.inc
+
+for.i.inc:
+ %j.next = add i32 %j, 1
+ %j.done = icmp eq i32 %j.next, 3
+ br i1 %j.done, label %exit, label %for.i
+
+exit:
+ ret i32 0
+}
>From 7a386c87febc93920f679c7a37d5f7b1cfd742b5 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Wed, 3 Jun 2026 22:12:04 -0700
Subject: [PATCH 2/3] fixup! add some more details
---
.../lib/Transforms/Scalar/LoopInterchange.cpp | 23 ++++++++++++-------
.../LoopInterchange/currentLimitation.ll | 10 +++-----
.../LoopInterchange/lcssa-preheader.ll | 13 ++++-------
.../loop-interchange-optimization-remarks.ll | 10 +++-----
4 files changed, 25 insertions(+), 31 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
index ba0a3f14019c0..82b927e3adbad 100644
--- a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
@@ -839,14 +839,21 @@ bool LoopInterchangeLegality::tightlyNested(Loop *OuterLoop, Loop *InnerLoop) {
// guarded-off ones. That is illegal when the inner loop relies on the guard
// to terminate.
//
- // TODO: Bailing out is conservative; such a nest can be interchanged when
- // the guard is preserved by the transform. If the guard condition is
- // invariant across the interchanged pair, keep it wrapping the interchanged
- // nest so the inner loop is skipped for the outer iterations the guard
- // excludes, instead of routing the guard edge into the inner latch as
- // adjustLoopBranches() does today. Only bail when the guard depends on the
- // interchanged induction variables or the inner loop cannot be proven to
- // run zero times when the guard is false. See llvm/llvm-project#201273.
+ // TODO: This is conservative approach and under some circumstances, we can
+ // still allow the interchange. e.g.
+ // for (j = 0; j < 3; j++) {
+ // for (i = 0; i < 2; i++) {
+ // if (j != 0) // guard: invariant across (i,k)
+ // for (k = 0; ; k++) { // inner; eq-exit needs j>=1
+ // y[i][j][k] = x[j][k][i] + w[j][k][i];
+ // if (k + 1 == j) break;
+ // }
+ // }
+ // }
+ // Here the guard (j != 0) uses only the enclosing loop's IV j, not the
+ // interchanged IVs (i and k), so it is invariant across both interchanged
+ // loops and could be hoisted to gate the whole nest; the interchange would
+ // still be legal.
if (OuterLoopHeader->getTerminator()->getNumSuccessors() > 1 &&
is_contained(successors(OuterLoopHeader), OuterLoopLatch)) {
LLVM_DEBUG(dbgs() << "Outer loop header guards the inner loop\n");
diff --git a/llvm/test/Transforms/LoopInterchange/currentLimitation.ll b/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
index 8af9a4d0ad89d..898627b05eacb 100644
--- a/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
+++ b/llvm/test/Transforms/LoopInterchange/currentLimitation.ll
@@ -25,13 +25,9 @@ target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
; CHECK: Name: Dependence
; CHECK-NEXT: Function: interchange_01
-; %for.cond1.preheader guards the inner loop: it branches to the inner loop
-; (%for.body4) or straight to the outer latch (%for.cond.loopexit) based on
-; %cmp324 = (N-1 > 1). The inner loop exits on (inner IV == N-2), so it only
-; terminates because the guard stops it from running when N <= 2. Interchanging
-; would move the inner loop outside the guard and run it on every outer
-; iteration; with N == 2 (reachable, since the nest is entered for N > 1) the
-; inner exit is never taken and it loops forever.
+; Guarded nest: %for.cond1.preheader runs the inner loop only when
+; %cmp324 = (N-1 > 1). The inner exit (inner IV == N-2) only terminates under
+; that guard, so interchanging it loops forever for N == 2.
; DELIN: Name: NotTightlyNested
; DELIN-NEXT: Function: interchange_01
define void @interchange_01(i32 %k, i32 %N) {
diff --git a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
index 42a5fd1c6000e..10ec0cba63043 100644
--- a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
+++ b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
@@ -14,15 +14,10 @@ target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
; }
; }
-;; Without -da-disable-delinearization-checks this is not interchanged due to
-;; dependence. Even with that flag it must not be interchanged: %outer.header
-;; guards the inner loop by branching to it or to the outer latch based on
-;; %cmp222 = (m > 0). The inner loop exits on (iv.next != m) with a nuw IV, so
-;; it only terminates when m > 0. Interchanging would run the inner loop on
-;; every outer iteration; with m == 0 (and n > 0) the exit is never taken and
-;; it loops forever. Verified with lli: lcssa_08(1, 0) returns before
-;; interchange but hangs after it, so the CHECK-DELIN run below must also leave
-;; it un-interchanged.
+;; Not interchanged by default (dependence). Even with
+;; -da-disable-delinearization-checks it must not be: %outer.header runs the
+;; inner loop only when %cmp222 = (m > 0), and its exit (iv.next != m, nuw)
+;; only terminates then, so interchanging it loops forever for m == 0.
define void @lcssa_08(i32 %n, i32 %m) {;
; CHECK-LABEL: define void @lcssa_08(
; CHECK-SAME: i32 [[N:%.*]], i32 [[M:%.*]]) {
diff --git a/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll b/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
index 5c41bb9e84c92..b656f15bb92d1 100644
--- a/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
+++ b/llvm/test/Transforms/LoopInterchange/loop-interchange-optimization-remarks.ll
@@ -159,13 +159,9 @@ define void @test02(i32 %k, i32 %N) {
; DELIN-NEXT: - String: Computed dependence info, invoking the transform.
; DELIN-NEXT: ...
-; %for.cond1.preheader guards the inner loop: it branches to the inner loop
-; (%for.body4) or straight to the outer latch (%for.cond.loopexit) based on
-; %cmp324 = (N-1 > 1). The inner loop exits on (inner IV == N-2), so it only
-; terminates because the guard stops it from running when N <= 2. Interchanging
-; would move the inner loop outside the guard and run it on every outer
-; iteration; with N == 2 (reachable, since the nest is entered for N > 1) the
-; inner exit is never taken and it loops forever.
+; Guarded nest: %for.cond1.preheader runs the inner loop only when
+; %cmp324 = (N-1 > 1). The inner exit (inner IV == N-2) only terminates under
+; that guard, so interchanging it loops forever for N == 2.
; DELIN: --- !Missed
; DELIN-NEXT: Pass: loop-interchange
; DELIN-NEXT: Name: NotTightlyNested
>From 87151796132820870f5932ea67703517a91c8c12 Mon Sep 17 00:00:00 2001
From: Madhur Amilkanthwar <madhura at nvidia.com>
Date: Thu, 4 Jun 2026 07:51:47 -0700
Subject: [PATCH 3/3] fixup! address review comments
---
.../lib/Transforms/Scalar/LoopInterchange.cpp | 52 +++++++-----------
.../LoopInterchange/lcssa-preheader.ll | 2 +-
.../pr201273-guarded-inner-loop.ll | 54 +++++++++++--------
3 files changed, 52 insertions(+), 56 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
index 82b927e3adbad..f88d8c0cf5117 100644
--- a/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopInterchange.cpp
@@ -824,42 +824,28 @@ bool LoopInterchangeLegality::tightlyNested(Loop *OuterLoop, Loop *InnerLoop) {
<< "' and '" << InnerLoop->getName()
<< "' are tightly nested\n");
- // A perfectly nested loop will not have any branch in between the outer and
- // inner block i.e. outer header will branch to either inner preheader and
- // outerloop latch.
+ // In a perfectly nested loop the outer header branches only into the inner
+ // loop. If it can also reach the outer latch, it conditionally guards the
+ // inner loop (an imperfect nest), so the inner loop runs on only a subset of
+ // the outer iterations. Interchanging such a nest would run the inner loop on
+ // every outer iteration, including the guarded-off ones, which is illegal
+ // when the inner loop relies on the guard to terminate (e.g. an eq/ne exit
+ // whose trip count is degenerate once the guard is false). Reject by allowing
+ // the outer header to branch only into the inner loop.
+ //
+ // TODO: This is conservative. A guarded nest is still safe to interchange
+ // when the inner loop has a computable trip count that is empty exactly when
+ // the guard is false, e.g.:
+ // for (i = 0; i < N; i++)
+ // if (M > 0) // loop-invariant guard
+ // for (j = 0; j < M; j++) // empty when M <= 0
+ // A[j][i] = ...;
+ // Interchanging is legal here because the inner loop runs zero times on the
+ // guarded-off iterations.
for (BasicBlock *Succ : successors(OuterLoopHeader))
- if (Succ != InnerLoopPreHeader && Succ != InnerLoop->getHeader() &&
- Succ != OuterLoopLatch)
+ if (Succ != InnerLoopPreHeader && Succ != InnerLoop->getHeader())
return false;
- // Reject nests where the outer-loop header conditionally branches to the
- // outer latch. Such a branch guards the inner loop, so it runs only on a
- // subset of the outer iterations. If interchanged, the inner loop would move
- // outside the guard and run on every outer iteration, including the
- // guarded-off ones. That is illegal when the inner loop relies on the guard
- // to terminate.
- //
- // TODO: This is conservative approach and under some circumstances, we can
- // still allow the interchange. e.g.
- // for (j = 0; j < 3; j++) {
- // for (i = 0; i < 2; i++) {
- // if (j != 0) // guard: invariant across (i,k)
- // for (k = 0; ; k++) { // inner; eq-exit needs j>=1
- // y[i][j][k] = x[j][k][i] + w[j][k][i];
- // if (k + 1 == j) break;
- // }
- // }
- // }
- // Here the guard (j != 0) uses only the enclosing loop's IV j, not the
- // interchanged IVs (i and k), so it is invariant across both interchanged
- // loops and could be hoisted to gate the whole nest; the interchange would
- // still be legal.
- if (OuterLoopHeader->getTerminator()->getNumSuccessors() > 1 &&
- is_contained(successors(OuterLoopHeader), OuterLoopLatch)) {
- LLVM_DEBUG(dbgs() << "Outer loop header guards the inner loop\n");
- return false;
- }
-
LLVM_DEBUG(dbgs() << "Checking instructions in Loop header and Loop latch\n");
// The inner loop reduction pattern requires storing the LCSSA PHI in
diff --git a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
index 10ec0cba63043..1e251bed0d5f7 100644
--- a/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
+++ b/llvm/test/Transforms/LoopInterchange/lcssa-preheader.ll
@@ -16,7 +16,7 @@ target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
;; Not interchanged by default (dependence). Even with
;; -da-disable-delinearization-checks it must not be: %outer.header runs the
-;; inner loop only when %cmp222 = (m > 0), and its exit (iv.next != m, nuw)
+;; inner loop only when %cmp222 = (m >s 0), and its exit (iv.next != m)
;; only terminates then, so interchanging it loops forever for m == 0.
define void @lcssa_08(i32 %n, i32 %m) {;
; CHECK-LABEL: define void @lcssa_08(
diff --git a/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll b/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
index 2805e7b8a42a7..681dca5bbcba3 100644
--- a/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
+++ b/llvm/test/Transforms/LoopInterchange/pr201273-guarded-inner-loop.ll
@@ -1,12 +1,22 @@
-; RUN: opt < %s -passes=loop-interchange -cache-line-size=64 \
+; RUN: opt < %s -passes=loop-interchange \
+; RUN: -loop-interchange-profitabilities=ignore \
; RUN: -pass-remarks-missed='loop-interchange' -disable-output \
; RUN: -pass-remarks-output=%t
; RUN: FileCheck -input-file=%t %s
-; The middle loop header (%for.j) guards the inner loop %for.k: the inner loop
-; only runs when %j != 0, and its exit condition (%k.next == %j) is only
-; well-defined under that guard. Hoisting %for.k out of the guard would make it
-; spin when %j == 0, so the pass must not interchange this nest.
+; The middle loop %for.j guards the inner loop %for.k: %for.k runs only when
+; %i != 0 (the outer loop's IV), and its exit %k.next == %i is only well-defined
+; under that guard. Interchanging %for.j and %for.k would run %for.k on every
+; iteration and spin when %i == 0, so the pass must not interchange this nest.
+;
+; Pseudo code:
+; for (i = 0; i < 3; i++)
+; for (j = 0; j < 2; j++)
+; if (i != 0) // guard on the outer IV
+; for (k = 0; ; k++) { // terminates only when i != 0
+; y[j][i][k] = x[i][k][j] + w[i][k][j];
+; if (k + 1 == i) break;
+; }
; CHECK: --- !Missed
; CHECK-NEXT: Pass: loop-interchange
@@ -22,21 +32,21 @@ entry:
br label %for.i
for.i:
- %j = phi i32 [ %j.next, %for.i.inc ], [ 0, %entry ]
- %j.is.zero = icmp eq i32 %j, 0
- %xbase = getelementptr [9 x i32], ptr @x, i32 %j
- %wbase = getelementptr [9 x i32], ptr @w, i32 %j
- %ybase = getelementptr [3 x i32], ptr @y, i32 %j
+ %i = phi i32 [ %i.next, %for.i.inc ], [ 0, %entry ]
+ %i.is.zero = icmp eq i32 %i, 0
+ %xbase = getelementptr [9 x i32], ptr @x, i32 %i
+ %wbase = getelementptr [9 x i32], ptr @w, i32 %i
+ %ybase = getelementptr [3 x i32], ptr @y, i32 %i
br label %for.j
for.j:
- %i = phi i32 [ %i.next, %for.j.inc ], [ 0, %for.i ]
- br i1 %j.is.zero, label %for.j.inc, label %for.k.ph
+ %j = phi i32 [ %j.next, %for.j.inc ], [ 0, %for.i ]
+ br i1 %i.is.zero, label %for.j.inc, label %for.k.ph
for.k.ph:
- %xp = getelementptr i32, ptr %xbase, i32 %i
- %wp = getelementptr i32, ptr %wbase, i32 %i
- %yp = getelementptr [9 x i32], ptr %ybase, i32 %i
+ %xp = getelementptr i32, ptr %xbase, i32 %j
+ %wp = getelementptr i32, ptr %wbase, i32 %j
+ %yp = getelementptr [9 x i32], ptr %ybase, i32 %j
br label %for.k
for.k:
@@ -49,18 +59,18 @@ for.k:
%yk = getelementptr i32, ptr %yp, i32 %k
store i32 %add, ptr %yk, align 4
%k.next = add i32 %k, 1
- %k.done = icmp eq i32 %k.next, %j
+ %k.done = icmp eq i32 %k.next, %i
br i1 %k.done, label %for.j.inc, label %for.k
for.j.inc:
- %i.next = add i32 %i, 1
- %i.done = icmp eq i32 %i, 0
- br i1 %i.done, label %for.j, label %for.i.inc
+ %j.next = add i32 %j, 1
+ %j.cmp = icmp eq i32 %j, 0
+ br i1 %j.cmp, label %for.j, label %for.i.inc
for.i.inc:
- %j.next = add i32 %j, 1
- %j.done = icmp eq i32 %j.next, 3
- br i1 %j.done, label %exit, label %for.i
+ %i.next = add i32 %i, 1
+ %i.done = icmp eq i32 %i.next, 3
+ br i1 %i.done, label %exit, label %for.i
exit:
ret i32 0
More information about the llvm-commits
mailing list