[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