[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