[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