[llvm] [LoopUnroll] Unroll when requested despite inlining candidates (PR #219077)

Aiden Grossman via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 13:56:54 PDT 2026


https://github.com/boomanaiden154 updated https://github.com/llvm/llvm-project/pull/219077

>From f90aed49034eedf085a3892c232835c63d4d476a Mon Sep 17 00:00:00 2001
From: Aiden Grossman <aidengrossman at google.com>
Date: Thu, 27 Aug 2026 00:12:00 +0000
Subject: [PATCH 1/2] [LoopUnroll] Unroll when requested despite inlining
 candidates

The LoopUnroller uses a heuristic when deciding to unroll where it will
not unroll if there are "inlining candidates" (defined by CodeMetrics as
single user internal functions), even if the user has specified
unrolling with a pragma. Ending up with a warning for an unrollable loop
that does not get unrolled due to a heuristic is confusing to users and
likely not what they want.

This can potentially lead to explosive inlining, although we did not
observe any significant instances of that when compiling our ~2B LOC
codebase.

Co-Authored-By: Justin Fargnoli <jfargnoli at nvidia.com>
---
 llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp | 16 ++----
 .../LoopUnroll/debug-and-remarks.ll           |  4 +-
 .../unroll_with_call_with_request.ll          | 55 +++++++++++++++++++
 3 files changed, 61 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll

diff --git a/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp b/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
index ad3b123f3327c..544d3b2630ee9 100644
--- a/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
@@ -1326,18 +1326,12 @@ tryToUnrollLoop(Loop *L, DominatorTree &DT, LoopInfo *LI, ScalarEvolution &SE,
   if (OptForSize)
     UP.Threshold = std::max(UP.Threshold, LoopSize + 1);
 
-  if (UCE.NumInlineCandidates != 0) {
-    LLVM_DEBUG(dbgs().indent(1)
-               << "Not unrolling loop with inlinable calls.\n");
-    if (TM & TM_ForcedByUser) {
-      ORE.emit([&]() {
-        return OptimizationRemarkMissed(DEBUG_TYPE,
-                                        "InlineCandidatesPreventUnroll",
-                                        L->getStartLoc(), L->getHeader())
-               << "unable to unroll loop: contains inlinable calls";
-      });
+  if (!(TM & TM_ForcedByUser)) {
+    if (UCE.NumInlineCandidates != 0) {
+      LLVM_DEBUG(dbgs().indent(1)
+                 << "Not unrolling loop with inlinable calls.\n");
+      return LoopUnrollResult::Unmodified;
     }
-    return LoopUnrollResult::Unmodified;
   }
 
   // Find the smallest exact trip count for any exit. This is an upper bound
diff --git a/llvm/test/Transforms/LoopUnroll/debug-and-remarks.ll b/llvm/test/Transforms/LoopUnroll/debug-and-remarks.ll
index 3cf92dbbf9315..711830e16efb4 100644
--- a/llvm/test/Transforms/LoopUnroll/debug-and-remarks.ll
+++ b/llvm/test/Transforms/LoopUnroll/debug-and-remarks.ll
@@ -180,7 +180,6 @@ exit:
 ; CHECK-LABEL:Loop Unroll: F[inline_prevents_unroll] Loop %for.body (depth=1)
 ; CHECK-NEXT:Loop Size = 8
 ; CHECK-NEXT: Not unrolling loop with inlinable calls.
-; CHECK-NEXT:remark: <unknown>:0:0: unable to unroll loop: contains inlinable calls
 
 define internal i32 @single_use_helper(i32 %x) {
   %add = add i32 %x, 42
@@ -200,7 +199,7 @@ for.body:
   %add = add i32 %sum, %helper_result
   %inc = add i32 %i, 1
   %cmp = icmp ult i32 %inc, 10
-  br i1 %cmp, label %for.body, label %exit, !llvm.loop !2
+  br i1 %cmp, label %for.body, label %exit
 
 exit:
   ret i32 %add
@@ -1007,7 +1006,6 @@ exit:
 
 !0 = distinct !{!0, !3}
 !1 = distinct !{!1, !4}
-!2 = distinct !{!2, !4}
 !3 = !{!"llvm.loop.unroll.full"}
 !4 = !{!"llvm.loop.unroll.enable"}
 !5 = distinct !{!5, !6}
diff --git a/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll b/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll
new file mode 100644
index 0000000000000..5d605f731e7a1
--- /dev/null
+++ b/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll
@@ -0,0 +1,55 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -passes=loop-unroll -S < %s | FileCheck %s
+
+; Check that we unroll loops when requested by the user (e.g., through a
+; pragma), even if there are inlining candidates in the loop (one of the
+; heuristics used by the unroller).
+
+define internal i32 @single_use_helper(i32 %x) {
+; CHECK-LABEL: define internal i32 @single_use_helper(
+; CHECK-SAME: i32 [[X:%.*]]) {
+; CHECK-NEXT:    [[ADD:%.*]] = add i32 [[X]], 42
+; CHECK-NEXT:    ret i32 [[ADD]]
+;
+  %add = add i32 %x, 42
+  ret i32 %add
+}
+
+define i32 @inline_prevents_unroll(ptr %A) {
+; CHECK-LABEL: define i32 @inline_prevents_unroll(
+; CHECK-SAME: ptr [[A:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    br label %[[FOR_BODY:.*]]
+; CHECK:       [[FOR_BODY]]:
+; CHECK-NEXT:    [[LOAD1:%.*]] = load i32, ptr [[A]], align 4
+; CHECK-NEXT:    [[SUM:%.*]] = call i32 @single_use_helper(i32 [[LOAD1]])
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds i32, ptr [[A]], i32 1
+; CHECK-NEXT:    [[LOAD:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; CHECK-NEXT:    [[HELPER_RESULT:%.*]] = call i32 @single_use_helper(i32 [[LOAD]])
+; CHECK-NEXT:    [[ADD:%.*]] = add i32 [[SUM]], [[HELPER_RESULT]]
+; CHECK-NEXT:    [[ARRAYIDX_2:%.*]] = getelementptr inbounds i32, ptr [[A]], i32 2
+; CHECK-NEXT:    [[LOAD_2:%.*]] = load i32, ptr [[ARRAYIDX_2]], align 4
+; CHECK-NEXT:    [[HELPER_RESULT_2:%.*]] = call i32 @single_use_helper(i32 [[LOAD_2]])
+; CHECK-NEXT:    [[ADD_LCSSA:%.*]] = add i32 [[ADD]], [[HELPER_RESULT_2]]
+; CHECK-NEXT:    ret i32 [[ADD_LCSSA]]
+;
+entry:
+  br label %for.body
+
+for.body:
+  %i = phi i32 [ 0, %entry ], [ %inc, %for.body ]
+  %sum = phi i32 [ 0, %entry ], [ %add, %for.body ]
+  %arrayidx = getelementptr inbounds i32, ptr %A, i32 %i
+  %load = load i32, ptr %arrayidx
+  %helper_result = call i32 @single_use_helper(i32 %load)
+  %add = add i32 %sum, %helper_result
+  %inc = add i32 %i, 1
+  %cmp = icmp ult i32 %inc, 3
+  br i1 %cmp, label %for.body, label %exit, !llvm.loop !0
+
+exit:
+  ret i32 %add
+}
+
+!0 = distinct !{!0, !1}
+!1 = !{!"llvm.loop.unroll.enable"}

>From 6583dd3870ba044c6508002b3f2e09c6641dc57c Mon Sep 17 00:00:00 2001
From: Aiden Grossman <aidengrossman at google.com>
Date: Fri, 28 Aug 2026 20:56:33 +0000
Subject: [PATCH 2/2] fix

---
 llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp |  2 +-
 .../unroll_with_call_with_request.ll          | 31 ++++++++++++++++++-
 2 files changed, 31 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp b/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
index 544d3b2630ee9..031121959df63 100644
--- a/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopUnrollPass.cpp
@@ -1326,7 +1326,7 @@ tryToUnrollLoop(Loop *L, DominatorTree &DT, LoopInfo *LI, ScalarEvolution &SE,
   if (OptForSize)
     UP.Threshold = std::max(UP.Threshold, LoopSize + 1);
 
-  if (!(TM & TM_ForcedByUser)) {
+  if (!(TM & TM_ForcedByUser) || PrepareForLTO) {
     if (UCE.NumInlineCandidates != 0) {
       LLVM_DEBUG(dbgs().indent(1)
                  << "Not unrolling loop with inlinable calls.\n");
diff --git a/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll b/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll
index 5d605f731e7a1..5ff972fffec67 100644
--- a/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll
+++ b/llvm/test/Transforms/LoopUnroll/unroll_with_call_with_request.ll
@@ -1,11 +1,18 @@
 ; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -passes="loop-unroll<prepare-for-lto>" -S < %s | FileCheck %s --check-prefix=LTO-PREPARE
 ; RUN: opt -passes=loop-unroll -S < %s | FileCheck %s
 
 ; Check that we unroll loops when requested by the user (e.g., through a
 ; pragma), even if there are inlining candidates in the loop (one of the
-; heuristics used by the unroller).
+; heuristics used by the unroller). Also ensure that we do not unroll
+; such loops when preparing for (Thin)LTO.
 
 define internal i32 @single_use_helper(i32 %x) {
+; LTO-PREPARE-LABEL: define internal i32 @single_use_helper(
+; LTO-PREPARE-SAME: i32 [[X:%.*]]) {
+; LTO-PREPARE-NEXT:    [[ADD:%.*]] = add i32 [[X]], 42
+; LTO-PREPARE-NEXT:    ret i32 [[ADD]]
+;
 ; CHECK-LABEL: define internal i32 @single_use_helper(
 ; CHECK-SAME: i32 [[X:%.*]]) {
 ; CHECK-NEXT:    [[ADD:%.*]] = add i32 [[X]], 42
@@ -16,6 +23,24 @@ define internal i32 @single_use_helper(i32 %x) {
 }
 
 define i32 @inline_prevents_unroll(ptr %A) {
+; LTO-PREPARE-LABEL: define i32 @inline_prevents_unroll(
+; LTO-PREPARE-SAME: ptr [[A:%.*]]) {
+; LTO-PREPARE-NEXT:  [[ENTRY:.*]]:
+; LTO-PREPARE-NEXT:    br label %[[FOR_BODY:.*]]
+; LTO-PREPARE:       [[FOR_BODY]]:
+; LTO-PREPARE-NEXT:    [[I:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[INC:%.*]], %[[FOR_BODY]] ]
+; LTO-PREPARE-NEXT:    [[SUM:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[ADD:%.*]], %[[FOR_BODY]] ]
+; LTO-PREPARE-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds i32, ptr [[A]], i32 [[I]]
+; LTO-PREPARE-NEXT:    [[LOAD:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; LTO-PREPARE-NEXT:    [[HELPER_RESULT:%.*]] = call i32 @single_use_helper(i32 [[LOAD]])
+; LTO-PREPARE-NEXT:    [[ADD]] = add i32 [[SUM]], [[HELPER_RESULT]]
+; LTO-PREPARE-NEXT:    [[INC]] = add i32 [[I]], 1
+; LTO-PREPARE-NEXT:    [[CMP:%.*]] = icmp ult i32 [[INC]], 3
+; LTO-PREPARE-NEXT:    br i1 [[CMP]], label %[[FOR_BODY]], label %[[EXIT:.*]], !llvm.loop [[LOOP0:![0-9]+]]
+; LTO-PREPARE:       [[EXIT]]:
+; LTO-PREPARE-NEXT:    [[ADD_LCSSA:%.*]] = phi i32 [ [[ADD]], %[[FOR_BODY]] ]
+; LTO-PREPARE-NEXT:    ret i32 [[ADD_LCSSA]]
+;
 ; CHECK-LABEL: define i32 @inline_prevents_unroll(
 ; CHECK-SAME: ptr [[A:%.*]]) {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
@@ -53,3 +78,7 @@ exit:
 
 !0 = distinct !{!0, !1}
 !1 = !{!"llvm.loop.unroll.enable"}
+;.
+; LTO-PREPARE: [[LOOP0]] = distinct !{[[LOOP0]], [[META1:![0-9]+]]}
+; LTO-PREPARE: [[META1]] = !{!"llvm.loop.unroll.enable"}
+;.



More information about the llvm-commits mailing list