[llvm] [SPIR-V] Avoid exponential path enumeration in findPathsToMatch (PR #212933)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Jul 29 23:06:58 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-backend-spir-v
Author: Arseniy Obolenskiy (aobolensk)
<details>
<summary>Changes</summary>
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
---
Full diff: https://github.com/llvm/llvm-project/pull/212933.diff
2 Files Affected:
- (modified) llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp (+44-20)
- (modified) llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp (+102)
``````````diff
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"(
``````````
</details>
https://github.com/llvm/llvm-project/pull/212933
More information about the llvm-commits
mailing list