[llvm] [SPIR-V] Avoid exponential path enumeration in findPathsToMatch (PR #212933)
Arseniy Obolenskiy via llvm-commits
llvm-commits at lists.llvm.org
Wed Jul 29 23:06:10 PDT 2026
https://github.com/aobolensk created https://github.com/llvm/llvm-project/pull/212933
Replace the recursive traversal, which revisits the common suffix once per incoming path, with an iterative postorder scan so reachability is computed once per block
A chain of N reconverging diamonds previously took time exponential in N, now it is flat
Benchmark (opt -passes=spirv-merge-region-exits -disable-output), N diamonds, before fix vs after fix:
N=8 0.012s 0.011s
N=12 0.017s 0.011s
N=16 0.067s 0.012s
N=20 1.015s 0.012s
N=22 3.71s 0.012s
N=24 15.01s 0.013s
>From 0baa4aab1fe27019451e204e60d366bfda979c0f Mon Sep 17 00:00:00 2001
From: Arseniy Obolenskiy <arseniy.obolenskiy at amd.com>
Date: Thu, 30 Jul 2026 08:04:32 +0200
Subject: [PATCH] [SPIR-V] Avoid exponential path enumeration in
findPathsToMatch
Replace the recursive traversal, which revisits the common suffix once
per incoming path, with an iterative postorder scan so reachability is
computed once per block
A chain of N reconverging diamonds previously
took time exponential in N, now it is flat
Benchmark (opt -passes=spirv-merge-region-exits -disable-output),
N diamonds, before fix vs after fix:
N=8 0.012s 0.011s
N=12 0.017s 0.011s
N=16 0.067s 0.012s
N=20 1.015s 0.012s
N=22 3.71s 0.012s
N=24 15.01s 0.013s
---
.../SPIRVConvergenceRegionAnalysis.cpp | 64 +++++++----
.../SPIRVConvergenceRegionAnalysisTests.cpp | 102 ++++++++++++++++++
2 files changed, 146 insertions(+), 20 deletions(-)
diff --git a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
index f24877611f077..930edb1c4a7a9 100644
--- a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
+++ b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
@@ -15,6 +15,7 @@
#include "SPIRVConvergenceRegionAnalysis.h"
#include "SPIRV.h"
#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallVector.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/IR/Dominators.h"
#include "llvm/IR/IntrinsicInst.h"
@@ -213,29 +214,52 @@ class ConvergenceRegionAnalyzer {
SmallPtrSet<BasicBlock *, 0>
findPathsToMatch(LoopInfo &LI, BasicBlock *From,
std::function<bool(const BasicBlock *)> isMatch) const {
- SmallPtrSet<BasicBlock *, 0> Output;
-
- if (isMatch(From))
- Output.insert(From);
+ // Compute the postorder of the blocks forward-reachable from |From|,
+ // ignoring back edges. Successors therefore always appear before their
+ // predecessors in the resulting list.
+ SmallVector<BasicBlock *, 16> PostOrder;
+ SmallPtrSet<BasicBlock *, 16> Visited;
+ SmallVector<std::pair<BasicBlock *, unsigned>, 16> Stack;
+
+ Visited.insert(From);
+ Stack.push_back({From, 0});
+ while (!Stack.empty()) {
+ auto &[BB, NextSuccessor] = Stack.back();
+ auto *Terminator = BB->getTerminator();
+ if (NextSuccessor < Terminator->getNumSuccessors()) {
+ auto *To = Terminator->getSuccessor(NextSuccessor++);
+ if (isBackEdge(BB, To) || !Visited.insert(To).second)
+ continue;
+ Stack.push_back({To, 0});
+ } else {
+ PostOrder.push_back(BB);
+ Stack.pop_back();
+ }
+ }
- auto *Terminator = From->getTerminator();
- for (unsigned i = 0; i < Terminator->getNumSuccessors(); ++i) {
- auto *To = Terminator->getSuccessor(i);
- // Ignore back edges.
- if (isBackEdge(From, To))
- continue;
+ // Propagate reachability to a matching block backward: a block belongs
+ // to the output if it matches, or if one of its non-back-edge successors
+ // does. Successors precede predecessors in |PostOrder|, so a single
+ // linear scan is enough, no per-path recursion.
+ SmallPtrSet<BasicBlock *, 0> Output;
+ for (auto *BB : PostOrder) {
+ bool ReachesMatch = false;
+ auto *Terminator = BB->getTerminator();
+ for (unsigned i = 0; i < Terminator->getNumSuccessors(); ++i) {
+ auto *To = Terminator->getSuccessor(i);
+ if (!isBackEdge(BB, To) && Output.contains(To))
+ ReachesMatch = true;
+ }
- auto ChildSet = findPathsToMatch(LI, To, isMatch);
- if (ChildSet.size() == 0)
- continue;
+ if (isMatch(BB) || ReachesMatch)
+ Output.insert(BB);
- Output.insert(ChildSet.begin(), ChildSet.end());
- Output.insert(From);
- if (LI.isLoopHeader(From)) {
- auto *L = LI.getLoopFor(From);
- for (auto *BB : L->getBlocks()) {
- Output.insert(BB);
- }
+ // Preserve whole-loop inclusion: a qualifying path crossing a loop
+ // header brings in the entire loop.
+ if (ReachesMatch && LI.isLoopHeader(BB)) {
+ auto *L = LI.getLoopFor(BB);
+ for (auto *LoopBB : L->getBlocks())
+ Output.insert(LoopBB);
}
}
diff --git a/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp b/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
index 01105989774af..32e0589440696 100644
--- a/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
+++ b/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
@@ -954,6 +954,108 @@ TEST_F(SPIRVConvergenceRegionAnalysisTest,
{"", "l1_end", "end", "d"});
}
+// A chain of reconverging diamonds followed by a call matching the loop's
+// convergence token. Regression test for findPathsToMatch: a naive recursive
+// enumeration of paths would revisit the common suffix once per incoming
+// path, growing exponentially with the number of diamonds.
+TEST_F(SPIRVConvergenceRegionAnalysisTest,
+ ReconvergingDiamondsWithConvergenceBranch) {
+ StringRef Assembly = R"(
+ define void @main() convergent "hlsl.numthreads"="4,8,16" "hlsl.shader"="compute" {
+ %t1 = call token @llvm.experimental.convergence.entry()
+ %1 = icmp ne i32 0, 0
+ br label %l1_header
+
+ l1_header:
+ %tl1 = call token @llvm.experimental.convergence.loop() [ "convergencectrl"(token %t1) ]
+ br i1 %1, label %l1_body, label %l1_end
+
+ l1_body:
+ %2 = icmp ne i32 0, 0
+ br i1 %2, label %l1_condition_true, label %l1_condition_false
+
+ l1_condition_true:
+ br label %d0
+
+ d0:
+ br i1 %1, label %d0_true, label %d0_false
+
+ d0_true:
+ br label %d0_join
+
+ d0_false:
+ br label %d0_join
+
+ d0_join:
+ br label %d1
+
+ d1:
+ br i1 %1, label %d1_true, label %d1_false
+
+ d1_true:
+ br label %d1_join
+
+ d1_false:
+ br label %d1_join
+
+ d1_join:
+ br label %d2
+
+ d2:
+ br i1 %1, label %d2_true, label %d2_false
+
+ d2_true:
+ br label %d2_join
+
+ d2_false:
+ br label %d2_join
+
+ d2_join:
+ br label %c
+
+ c:
+ %call = call spir_func i32 @_Z3absi(i32 0) [ "convergencectrl"(token %tl1) ]
+ br label %end
+
+ l1_condition_false:
+ br label %l1_continue
+
+ l1_continue:
+ br label %l1_header
+
+ l1_end:
+ br label %end
+
+ end:
+ ret void
+ }
+
+ declare token @llvm.experimental.convergence.entry()
+ declare token @llvm.experimental.convergence.control()
+ declare token @llvm.experimental.convergence.loop()
+
+ ; This intrinsic is not convergent. This is only because the backend doesn't
+ ; support convergent operations yet.
+ declare spir_func i32 @_Z3absi(i32) convergent
+ )";
+
+ runAnalysis(Assembly).getTopLevelRegion();
+ const auto *L = getRegionWithEntry("l1_header");
+ ASSERT_NE(L, nullptr);
+
+ EXPECT_EQ(L->Entry, getBlock("l1_header"));
+ EXPECT_EQ(L->Exits.size(), 2ul);
+ EXPECT_THAT(L->Exits, ContainsBasicBlock("l1_header"));
+ EXPECT_THAT(L->Exits, ContainsBasicBlock("c"));
+
+ checkRegionBlocks(L,
+ {"l1_header", "l1_body", "l1_continue",
+ "l1_condition_false", "l1_condition_true", "d0", "d0_true",
+ "d0_false", "d0_join", "d1", "d1_true", "d1_false",
+ "d1_join", "d2", "d2_true", "d2_false", "d2_join", "c"},
+ {"", "l1_end", "end"});
+}
+
TEST_F(SPIRVConvergenceRegionAnalysisTest,
SingleLoopWithNoConvergenceIntrinsics) {
StringRef Assembly = R"(
More information about the llvm-commits
mailing list