[llvm] 90a8d09 - [CSSPGO] [Pseudo-Probe] Avoid infinite loop on cyclic invoke destinations (#221630)

via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 17 19:52:40 PDT 2026


Author: chandan singh
Date: 2026-09-18T08:22:34+05:30
New Revision: 90a8d09917f0d508292405a68ae1f6de3b40fd93

URL: https://github.com/llvm/llvm-project/commit/90a8d09917f0d508292405a68ae1f6de3b40fd93
DIFF: https://github.com/llvm/llvm-project/commit/90a8d09917f0d508292405a68ae1f6de3b40fd93.diff

LOG: [CSSPGO] [Pseudo-Probe] Avoid infinite loop on cyclic invoke destinations (#221630)

SampleProfileProber::getOriginalTerminator() walks invoke normal
destinations (and ignored single-successor blocks) so the CFG hash stays
stable across call-to-invoke conversion. That walk assumes the chain is
acyclic.

Valid IR can have a self-looping invoke (normal dest == the invoke
block). The walk then never returns, and opt -passes=pseudo-probe hangs.

Example Testcase:
```cpp
void f1();
void test() {
  try {
    for (;;)
      f1();
  } catch (...) {}
}
```

This patch walk iteratively and record visited blocks. If the next block
was already seen, stop at the terminator that closes the cycle. Acyclic
invoke continuation chains are unchanged.

Added: 
    llvm/test/Transforms/SampleProfile/pseudo-probe-call-vs-invoke-hash.ll
    llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll

Modified: 
    llvm/lib/Transforms/IPO/SampleProfileProbe.cpp

Removed: 
    


################################################################################
diff  --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index a62d76010f678..7f1227dea09ee 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -11,6 +11,8 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Transforms/IPO/SampleProfileProbe.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/Statistic.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/BlockFrequencyInfo.h"
@@ -218,15 +220,58 @@ void SampleProfileProber::findUnreachableBlocks(
   }
 }
 
+// Follow invoke normal-dest edges and record blocks that sit on a cycle.
+static void
+findInvokeNormalDestCycles(const Function &F,
+                           DenseSet<const BasicBlock *> &CycleBlocks) {
+  DenseSet<const BasicBlock *> Processed;
+  DenseSet<const BasicBlock *> OnCurrentPath;
+  SmallVector<const BasicBlock *, 16> CurrentPath;
+
+  for (const BasicBlock &Start : F) {
+    if (Processed.contains(&Start))
+      continue;
+
+    CurrentPath.clear();
+    OnCurrentPath.clear();
+    const BasicBlock *Cur = &Start;
+    while (Cur) {
+      if (OnCurrentPath.contains(Cur)) {
+        // Back-edge onto CurrentPath: the cycle is the suffix starting at Cur.
+        auto CycleStart = llvm::find(CurrentPath, Cur);
+        assert(CycleStart != CurrentPath.end() &&
+               "OnCurrentPath must hold exactly the blocks in CurrentPath");
+        CycleBlocks.insert(CycleStart, CurrentPath.end());
+        break;
+      }
+      if (Processed.contains(Cur))
+        break;
+      OnCurrentPath.insert(Cur);
+      CurrentPath.push_back(Cur);
+      if (const auto *II = dyn_cast<InvokeInst>(Cur->getTerminator()))
+        Cur = II->getNormalDest();
+      else
+        Cur = nullptr;
+    }
+    for (const BasicBlock *B : CurrentPath)
+      Processed.insert(B);
+  }
+}
+
 // In call-to-invoke conversion, basic block can be split into multiple blocks,
 // only instrument probe in the head block, ignore the normal dests.
 void SampleProfileProber::findInvokeNormalDests(
     DenseSet<BasicBlock *> &InvokeNormalDests) {
+  DenseSet<const BasicBlock *> CycleBlocks;
+  findInvokeNormalDestCycles(*F, CycleBlocks);
+
   for (auto &BB : *F) {
     auto *TI = BB.getTerminator();
     if (auto *II = dyn_cast<InvokeInst>(TI)) {
       auto *ND = II->getNormalDest();
-      InvokeNormalDests.insert(ND);
+      // Cycle members are original loop blocks, not split continuations.
+      if (!CycleBlocks.contains(ND))
+        InvokeNormalDests.insert(ND);
 
       // The normal dest and the try/catch block are connected by an
       // unconditional branch.
@@ -249,15 +294,28 @@ void SampleProfileProber::findInvokeNormalDests(
 // the tail block's successors are the original block's successors.
 const Instruction *SampleProfileProber::getOriginalTerminator(
     const BasicBlock *Head, const DenseSet<BasicBlock *> &BlocksToIgnore) {
-  auto *TI = Head->getTerminator();
-  if (auto *II = dyn_cast<InvokeInst>(TI)) {
-    return getOriginalTerminator(II->getNormalDest(), BlocksToIgnore);
-  } else if (succ_size(Head) == 1 &&
-             BlocksToIgnore.contains(*succ_begin(Head))) {
-    // Go to the unconditional branch dest.
-    return getOriginalTerminator(*succ_begin(Head), BlocksToIgnore);
+  // Follow invoke dests and ignored blocks to the original terminator. Stop
+  // if a block repeats; a cycle of invokes has no unique tail.
+  DenseSet<const BasicBlock *> Visited;
+  const BasicBlock *BB = Head;
+  Visited.insert(BB);
+  while (true) {
+    auto *TI = BB->getTerminator();
+    const BasicBlock *Next = nullptr;
+    if (const auto *II = dyn_cast<InvokeInst>(TI))
+      Next = II->getNormalDest();
+    else if (succ_size(BB) == 1 && BlocksToIgnore.contains(*succ_begin(BB)))
+      Next = *succ_begin(BB);
+    else
+      return TI;
+
+    // A cycle has no tail block whose terminator represents the original
+    // block. Stop at the terminator that closes the cycle.
+    if (!Visited.insert(Next).second)
+      return TI;
+
+    BB = Next;
   }
-  return TI;
 }
 
 // Compute Hash value for the CFG: the lower 32 bits are CRC32 of the index

diff  --git a/llvm/test/Transforms/SampleProfile/pseudo-probe-call-vs-invoke-hash.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-call-vs-invoke-hash.ll
new file mode 100644
index 0000000000000..ee13c374b853f
--- /dev/null
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-call-vs-invoke-hash.ll
@@ -0,0 +1,99 @@
+; RUN: opt < %s -passes=pseudo-probe -S | FileCheck %s
+
+; CFG hashes in !llvm.pseudo_probe_desc must match between a nounwind/call
+; shape and the equivalent invoke shape, so AutoFDO can use one profile.
+
+declare void @f0()
+declare void @f1()
+declare void @f2()
+declare i32 @__gxx_personality_v0(...)
+
+; for (;;) f1();
+define void @selfloop_calls() {
+entry:
+  br label %loop
+
+loop:
+  call void @f1()
+  br label %loop
+}
+
+define void @selfloop_invokes() personality ptr @__gxx_personality_v0 {
+entry:
+  br label %loop
+
+loop:
+  invoke void @f1()
+          to label %loop unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+          cleanup
+  ret void
+}
+
+; f0(); for (;;) f1();  -- loop header is also the invoke continuation
+define void @prefix_then_loop_calls() {
+entry:
+  call void @f0()
+  br label %loop
+
+loop:
+  call void @f1()
+  br label %loop
+}
+
+define void @prefix_then_loop_invokes() personality ptr @__gxx_personality_v0 {
+entry:
+  invoke void @f0()
+          to label %loop unwind label %lpad
+
+loop:
+  invoke void @f1()
+          to label %loop unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+          cleanup
+  ret void
+}
+
+; for (;;) { f1(); f2(); }
+define void @twocall_loop_calls() {
+entry:
+  br label %loop
+
+loop:
+  call void @f1()
+  call void @f2()
+  br label %loop
+}
+
+define void @twocall_loop_invokes() personality ptr @__gxx_personality_v0 {
+entry:
+  br label %for.cond
+
+for.cond:
+  invoke void @f1()
+          to label %invoke.cont unwind label %lpad
+
+invoke.cont:
+  invoke void @f2()
+          to label %for.cond unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+          cleanup
+  ret void
+}
+
+; CHECK: !{i64 {{-?[0-9]+}}, i64 [[H1:[0-9]+]], !"selfloop_calls"}
+; CHECK: !{i64 {{-?[0-9]+}}, i64 [[H1]], !"selfloop_invokes"}
+
+; CHECK: !{i64 {{-?[0-9]+}}, i64 [[H2:[0-9]+]], !"prefix_then_loop_calls"}
+; CHECK: !{i64 {{-?[0-9]+}}, i64 [[H2]], !"prefix_then_loop_invokes"}
+
+; FIXME: The invoke form needs two blocks where the call form needs one, so
+; the hashes cannot match yet. Hardcoded so a future fix fails this test.
+; CHECK: !{i64 {{-?[0-9]+}}, i64 562986241617760, !"twocall_loop_calls"}
+; CHECK: !{i64 {{-?[0-9]+}}, i64 563004266919717, !"twocall_loop_invokes"}

diff  --git a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
new file mode 100644
index 0000000000000..407e0f73a7b9f
--- /dev/null
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
@@ -0,0 +1,159 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=pseudo-probe -S | FileCheck %s
+
+; getOriginalTerminator walks invoke normal destinations. If that chain is a
+; cycle, the walk must stop. These CFGs loop infinitely without cycle detection.
+
+; Self-looping invoke: clang++ -O2 -fexceptions on
+;   try { for (;;) f1(); } catch (...) {}
+define void @self_looping_invoke() personality ptr @__gxx_personality_v0 {
+; CHECK-LABEL: define void @self_looping_invoke() personality ptr @__gxx_personality_v0 {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 1782689716240936679, i64 1, i32 0, i64 -1)
+; CHECK-NEXT:    br label %[[LOOP:.*]]
+; CHECK:       [[LOOP]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 1782689716240936679, i64 2, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f1()
+; CHECK-NEXT:            to label %[[LOOP]] unwind label %[[LPAD:.*]]
+; CHECK:       [[LPAD]]:
+; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
+; CHECK-NEXT:            cleanup
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  invoke void @f1()
+  to label %loop unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+  cleanup
+  ret void
+}
+
+; Two-invoke cycle: clang++ -O2 -fexceptions on
+;   try { for (;;) { f1(); f2(); } } catch (...) {}
+define void @two_invoke_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-LABEL: define void @two_invoke_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 8105103579929771406, i64 1, i32 0, i64 -1)
+; CHECK-NEXT:    br label %[[FOR_COND:.*]]
+; CHECK:       [[FOR_COND]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 8105103579929771406, i64 2, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f1()
+; CHECK-NEXT:            to label %[[INVOKE_CONT:.*]] unwind label %[[LPAD:.*]]
+; CHECK:       [[INVOKE_CONT]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 8105103579929771406, i64 4, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f2()
+; CHECK-NEXT:            to label %[[FOR_COND]] unwind label %[[LPAD]]
+; CHECK:       [[LPAD]]:
+; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
+; CHECK-NEXT:            cleanup
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %for.cond
+
+for.cond:
+  invoke void @f1()
+  to label %invoke.cont unwind label %lpad
+
+invoke.cont:
+  invoke void @f2()
+  to label %for.cond unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+  cleanup
+  ret void
+}
+
+; Three-invoke cycle: try { for (;;) { f1(); f2(); f3(); } } catch (...) {}
+define void @three_invoke_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-LABEL: define void @three_invoke_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 3830051905911222256, i64 1, i32 0, i64 -1)
+; CHECK-NEXT:    br label %[[FOR_COND:.*]]
+; CHECK:       [[FOR_COND]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 3830051905911222256, i64 2, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f1()
+; CHECK-NEXT:            to label %[[CONT1:.*]] unwind label %[[LPAD:.*]]
+; CHECK:       [[CONT1]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 3830051905911222256, i64 4, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f2()
+; CHECK-NEXT:            to label %[[CONT2:.*]] unwind label %[[LPAD]]
+; CHECK:       [[CONT2]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 3830051905911222256, i64 6, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f3()
+; CHECK-NEXT:            to label %[[FOR_COND]] unwind label %[[LPAD]]
+; CHECK:       [[LPAD]]:
+; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
+; CHECK-NEXT:            cleanup
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %for.cond
+
+for.cond:
+  invoke void @f1()
+  to label %cont1 unwind label %lpad
+
+cont1:
+  invoke void @f2()
+  to label %cont2 unwind label %lpad
+
+cont2:
+  invoke void @f3()
+  to label %for.cond unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+  cleanup
+  ret void
+}
+
+; entry -> Y -> Z -> Z. Only Z is on the cycle; Y is a split continuation
+; of entry and must NOT be probed.
+; Source: try { f1(); f2(); for (;;) f3(); } catch (...) {}
+define void @tail_into_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-LABEL: define void @tail_into_cycle() personality ptr @__gxx_personality_v0 {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 -2928887130654220254, i64 1, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f1()
+; CHECK-NEXT:            to label %[[Y:.*]] unwind label %[[LPAD:.*]]
+; CHECK:       [[Y]]:
+; CHECK-NEXT:    invoke void @f2()
+; CHECK-NEXT:            to label %[[Z:.*]] unwind label %[[LPAD]]
+; CHECK:       [[Z]]:
+; CHECK-NEXT:    call void @llvm.pseudoprobe(i64 -2928887130654220254, i64 4, i32 0, i64 -1)
+; CHECK-NEXT:    invoke void @f3()
+; CHECK-NEXT:            to label %[[Z]] unwind label %[[LPAD]]
+; CHECK:       [[LPAD]]:
+; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
+; CHECK-NEXT:            cleanup
+; CHECK-NEXT:    ret void
+;
+entry:
+  invoke void @f1()
+  to label %Y unwind label %lpad
+
+Y:
+  invoke void @f2()
+  to label %Z unwind label %lpad
+
+Z:
+  invoke void @f3()
+  to label %Z unwind label %lpad
+
+lpad:
+  %eh = landingpad { ptr, i32 }
+  cleanup
+  ret void
+}
+
+declare void @f1()
+declare void @f2()
+declare void @f3()
+declare i32 @__gxx_personality_v0(...)


        


More information about the llvm-commits mailing list