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

chandan singh via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 15 02:07:04 PDT 2026


https://github.com/chandankds updated https://github.com/llvm/llvm-project/pull/221630

>From b9ab072e3df296425824b52431cc244bc996a476 Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Mon, 7 Sep 2026 04:32:13 +0000
Subject: [PATCH 1/6] [CSSPGO] [Pseudo-Probe] Avoid infinite loop on cyclic
 invoke destinations

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:
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.
---
 .../lib/Transforms/IPO/SampleProfileProbe.cpp | 32 ++++++++++++++-----
 .../pseudo-probe-cyclic-normal-dest.ll        | 32 +++++++++++++++++++
 2 files changed, 56 insertions(+), 8 deletions(-)
 create mode 100644 llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index a62d76010f678..7e69fa0b30764 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -11,6 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Transforms/IPO/SampleProfileProbe.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/Statistic.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/BlockFrequencyInfo.h"
@@ -249,15 +250,30 @@ 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);
+  // This walk follows invoke normal destinations and ignored single-successor
+  // blocks, which is a finite chain after call-to-invoke conversion. Valid IR
+  // can still contain a self-looping invoke (normal dest == this block). Track
+  // visited blocks so that case terminates instead of looping forever.
+  SmallPtrSet<const BasicBlock *, 8> 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-cyclic-normal-dest.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
new file mode 100644
index 0000000000000..bba7a921360c4
--- /dev/null
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
@@ -0,0 +1,32 @@
+; RUN: opt < %s -passes=pseudo-probe -S | FileCheck %s
+
+; getOriginalTerminator walks invoke normal destinations. If that chain is a
+; cycle (self-looping invoke), the walk must stop. This CFG hangs without
+; cycle detection.
+;
+; Matches 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(
+entry:
+; CHECK:      entry:
+; CHECK:        call void @llvm.pseudoprobe
+; CHECK-NEXT:   br label %loop
+  br label %loop
+
+loop:
+; CHECK:      loop:
+; CHECK-NEXT:   invoke void @may_throw()
+; CHECK-NEXT:           to label %loop unwind label %lpad
+  invoke void @may_throw()
+          to label %loop unwind label %lpad
+
+lpad:
+; CHECK:      lpad:
+; CHECK-NOT:    call void @llvm.pseudoprobe
+  %eh = landingpad { ptr, i32 }
+          cleanup
+  ret void
+}
+
+declare void @may_throw()
+declare i32 @__gxx_personality_v0(...)

>From 1bd969507d9ec255669a02a631ca9740e1839e34 Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Wed, 9 Sep 2026 03:35:15 +0000
Subject: [PATCH 2/6] [CSSPGO] [Pseudo-Probe] Probe self-looping invoke blocks

A self-looping invoke is the original block, not a split continuation,
so still insert a probe there. Autogenerate FileCheck to pin probe IDs.
---
 .../lib/Transforms/IPO/SampleProfileProbe.cpp |  5 +++-
 .../pseudo-probe-cyclic-normal-dest.ll        | 23 +++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index 7e69fa0b30764..5d5853e1ab873 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -227,7 +227,10 @@ void SampleProfileProber::findInvokeNormalDests(
     auto *TI = BB.getTerminator();
     if (auto *II = dyn_cast<InvokeInst>(TI)) {
       auto *ND = II->getNormalDest();
-      InvokeNormalDests.insert(ND);
+      // A self-looping invoke is the original block, not a split continuation.
+      // Still instrument it.
+      if (ND != &BB)
+        InvokeNormalDests.insert(ND);
 
       // The normal dest and the try/catch block are connected by an
       // unconditional branch.
diff --git a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
index bba7a921360c4..345d848992dc8 100644
--- a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
@@ -1,3 +1,4 @@
+; 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
@@ -6,23 +7,27 @@
 ;
 ; Matches 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(
+; 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 @may_throw()
+; CHECK-NEXT:            to label %[[LOOP]] unwind label %[[LPAD:.*]]
+; CHECK:       [[LPAD]]:
+; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
+; CHECK-NEXT:            cleanup
+; CHECK-NEXT:    ret void
+;
 entry:
-; CHECK:      entry:
-; CHECK:        call void @llvm.pseudoprobe
-; CHECK-NEXT:   br label %loop
   br label %loop
 
 loop:
-; CHECK:      loop:
-; CHECK-NEXT:   invoke void @may_throw()
-; CHECK-NEXT:           to label %loop unwind label %lpad
   invoke void @may_throw()
           to label %loop unwind label %lpad
 
 lpad:
-; CHECK:      lpad:
-; CHECK-NOT:    call void @llvm.pseudoprobe
   %eh = landingpad { ptr, i32 }
           cleanup
   ret void

>From 8f102d17867705910fba609f7e762b51810bd2e4 Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Wed, 9 Sep 2026 03:42:57 +0000
Subject: [PATCH 3/6] NFC: shorten self-looping invoke instrumentation comment

---
 llvm/lib/Transforms/IPO/SampleProfileProbe.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index 5d5853e1ab873..fa14e8ea50442 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -227,8 +227,7 @@ void SampleProfileProber::findInvokeNormalDests(
     auto *TI = BB.getTerminator();
     if (auto *II = dyn_cast<InvokeInst>(TI)) {
       auto *ND = II->getNormalDest();
-      // A self-looping invoke is the original block, not a split continuation.
-      // Still instrument it.
+      // instrument self-looping invoke in the original block
       if (ND != &BB)
         InvokeNormalDests.insert(ND);
 

>From f1285eb2eeff24dc6e149e5625ff42f13e788a6a Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Mon, 14 Sep 2026 09:40:00 +0000
Subject: [PATCH 4/6] [CSSPGO] [Pseudo-Probe] Probe invoke blocks on
 multi-block normal-dest cycles

ND != &BB only covered a self-loop. Walk the invoke-normal-dest chain and
keep probes on any cycle. Split continuations from call-to-invoke are
still ignored.
---
 .../lib/Transforms/IPO/SampleProfileProbe.cpp |  21 +++-
 .../pseudo-probe-cyclic-normal-dest.ll        | 101 ++++++++++++++++--
 2 files changed, 109 insertions(+), 13 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index fa14e8ea50442..4f0cdb10f3750 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -219,6 +219,19 @@ void SampleProfileProber::findUnreachableBlocks(
   }
 }
 
+// True if following invoke normal destinations from BB revisits a block.
+// That is a cycle of invokes, not a split continuation from call-to-invoke.
+static bool isOnInvokeNormalDestCycle(const BasicBlock *BB) {
+  SmallPtrSet<const BasicBlock *, 8> Visited;
+  while (Visited.insert(BB).second) {
+    auto *II = dyn_cast<InvokeInst>(BB->getTerminator());
+    if (!II)
+      return false;
+    BB = II->getNormalDest();
+  }
+  return true;
+}
+
 // 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(
@@ -227,8 +240,8 @@ void SampleProfileProber::findInvokeNormalDests(
     auto *TI = BB.getTerminator();
     if (auto *II = dyn_cast<InvokeInst>(TI)) {
       auto *ND = II->getNormalDest();
-      // instrument self-looping invoke in the original block
-      if (ND != &BB)
+      // instrument invoke blocks that form a normal-dest cycle
+      if (!isOnInvokeNormalDestCycle(ND))
         InvokeNormalDests.insert(ND);
 
       // The normal dest and the try/catch block are connected by an
@@ -254,8 +267,8 @@ const Instruction *SampleProfileProber::getOriginalTerminator(
     const BasicBlock *Head, const DenseSet<BasicBlock *> &BlocksToIgnore) {
   // This walk follows invoke normal destinations and ignored single-successor
   // blocks, which is a finite chain after call-to-invoke conversion. Valid IR
-  // can still contain a self-looping invoke (normal dest == this block). Track
-  // visited blocks so that case terminates instead of looping forever.
+  // can still contain a cycle of invokes. Track visited blocks so that case
+  // terminates instead of looping forever.
   SmallPtrSet<const BasicBlock *, 8> Visited;
   const BasicBlock *BB = Head;
   Visited.insert(BB);
diff --git a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
index 345d848992dc8..4ba73ec021c35 100644
--- a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
@@ -2,10 +2,10 @@
 ; RUN: opt < %s -passes=pseudo-probe -S | FileCheck %s
 
 ; getOriginalTerminator walks invoke normal destinations. If that chain is a
-; cycle (self-looping invoke), the walk must stop. This CFG hangs without
-; cycle detection.
-;
-; Matches clang++ -O2 -fexceptions on: try { for (;;) f1(); } catch (...) {}
+; cycle, the walk must stop. These CFGs hang 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:.*:]]
@@ -13,7 +13,7 @@ define void @self_looping_invoke() personality ptr @__gxx_personality_v0 {
 ; 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 @may_throw()
+; CHECK-NEXT:    invoke void @f1()
 ; CHECK-NEXT:            to label %[[LOOP]] unwind label %[[LPAD:.*]]
 ; CHECK:       [[LPAD]]:
 ; CHECK-NEXT:    [[EH:%.*]] = landingpad { ptr, i32 }
@@ -24,14 +24,97 @@ entry:
   br label %loop
 
 loop:
-  invoke void @may_throw()
-          to label %loop unwind label %lpad
+  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.
+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
+  cleanup
   ret void
 }
 
-declare void @may_throw()
+declare void @f1()
+declare void @f2()
+declare void @f3()
 declare i32 @__gxx_personality_v0(...)

>From 9215decc49d7fb47db3b9e5a3623238d74e2392d Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Tue, 15 Sep 2026 08:51:06 +0000
Subject: [PATCH 5/6] [CSSPGO] [Pseudo-Probe] Distinguish invoke-cycle members
 from tails

A dest that only reaches a cycle is still a call-to-invoke continuation
and must not get a block probe. Compute cycle membership once per
function and add a tail-into-cycle test.
---
 .../lib/Transforms/IPO/SampleProfileProbe.cpp | 60 +++++++++++++------
 .../pseudo-probe-cyclic-normal-dest.ll        | 43 ++++++++++++-
 2 files changed, 83 insertions(+), 20 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index 4f0cdb10f3750..c736dbf4d83f6 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -11,7 +11,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Transforms/IPO/SampleProfileProbe.h"
-#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/Statistic.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/BlockFrequencyInfo.h"
@@ -219,29 +218,56 @@ void SampleProfileProber::findUnreachableBlocks(
   }
 }
 
-// True if following invoke normal destinations from BB revisits a block.
-// That is a cycle of invokes, not a split continuation from call-to-invoke.
-static bool isOnInvokeNormalDestCycle(const BasicBlock *BB) {
-  SmallPtrSet<const BasicBlock *, 8> Visited;
-  while (Visited.insert(BB).second) {
-    auto *II = dyn_cast<InvokeInst>(BB->getTerminator());
-    if (!II)
-      return false;
-    BB = II->getNormalDest();
+// 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;
+  std::vector<const BasicBlock *> 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 = CurrentPath.begin();
+        while (*CycleStart != Cur)
+          ++CycleStart;
+        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);
   }
-  return true;
 }
 
 // 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();
-      // instrument invoke blocks that form a normal-dest cycle
-      if (!isOnInvokeNormalDestCycle(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
@@ -265,11 +291,9 @@ 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) {
-  // This walk follows invoke normal destinations and ignored single-successor
-  // blocks, which is a finite chain after call-to-invoke conversion. Valid IR
-  // can still contain a cycle of invokes. Track visited blocks so that case
-  // terminates instead of looping forever.
-  SmallPtrSet<const BasicBlock *, 8> Visited;
+  // 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) {
diff --git a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
index 4ba73ec021c35..407e0f73a7b9f 100644
--- a/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
+++ b/llvm/test/Transforms/SampleProfile/pseudo-probe-cyclic-normal-dest.ll
@@ -2,7 +2,7 @@
 ; 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 hang without cycle detection.
+; cycle, the walk must stop. These CFGs loop infinitely without cycle detection.
 
 ; Self-looping invoke: clang++ -O2 -fexceptions on
 ;   try { for (;;) f1(); } catch (...) {}
@@ -70,7 +70,7 @@ lpad:
   ret void
 }
 
-; Three-invoke cycle.
+; 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:.*:]]
@@ -114,6 +114,45 @@ lpad:
   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()

>From cb240b99a40d354c2f9c06aa4ef34c0d6aac5f4a Mon Sep 17 00:00:00 2001
From: chandankds <chandankds at gmail.com>
Date: Tue, 15 Sep 2026 09:06:50 +0000
Subject: [PATCH 6/6] NFC: clang-format findInvokeNormalDestCycles

---
 llvm/lib/Transforms/IPO/SampleProfileProbe.cpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
index c736dbf4d83f6..127c1beeb5926 100644
--- a/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
+++ b/llvm/lib/Transforms/IPO/SampleProfileProbe.cpp
@@ -219,8 +219,9 @@ 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) {
+static void
+findInvokeNormalDestCycles(const Function &F,
+                           DenseSet<const BasicBlock *> &CycleBlocks) {
   DenseSet<const BasicBlock *> Processed;
   DenseSet<const BasicBlock *> OnCurrentPath;
   std::vector<const BasicBlock *> CurrentPath;



More information about the llvm-commits mailing list