[llvm] [SPIR-V] Avoid exponential path enumeration in findPathsToMatch (PR #212933)

Arseniy Obolenskiy via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 30 07:24:36 PDT 2026


https://github.com/aobolensk updated https://github.com/llvm/llvm-project/pull/212933

>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 1/3] [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"(

>From d05c70b750baee97f1b88592d277c7ac7d53d78f Mon Sep 17 00:00:00 2001
From: Arseniy Obolenskiy <arseniy.obolenskiy at amd.com>
Date: Thu, 30 Jul 2026 15:56:27 +0200
Subject: [PATCH 2/3] Address comments

---
 .../SPIRVConvergenceRegionAnalysis.cpp        | 46 ++++++++++---------
 1 file changed, 24 insertions(+), 22 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
index 930edb1c4a7a9..3092ddb764c67 100644
--- a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
+++ b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
@@ -14,6 +14,7 @@
 
 #include "SPIRVConvergenceRegionAnalysis.h"
 #include "SPIRV.h"
+#include "llvm/ADT/PostOrderIterator.h"
 #include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Analysis/LoopInfo.h"
@@ -211,31 +212,32 @@ class ConvergenceRegionAnalyzer {
     return false;
   }
 
+  class ForwardPostOrderTraversal
+      : public PostOrderTraversalBase<ForwardPostOrderTraversal,
+                                      GraphTraits<BasicBlock *>> {
+    const ConvergenceRegionAnalyzer &Analyzer;
+
+  public:
+    ForwardPostOrderTraversal(const ConvergenceRegionAnalyzer &Analyzer,
+                              BasicBlock *From)
+        : Analyzer(Analyzer) {
+      init(From);
+    }
+
+    bool insertEdge(std::optional<BasicBlock *> From, BasicBlock *To) {
+      return !(From && Analyzer.isBackEdge(*From, To)) &&
+             Visited.insert(To).second;
+    }
+
+  private:
+    SmallPtrSet<BasicBlock *, 16> Visited;
+  };
+
   SmallPtrSet<BasicBlock *, 0>
   findPathsToMatch(LoopInfo &LI, BasicBlock *From,
                    std::function<bool(const BasicBlock *)> isMatch) const {
-    // 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();
-      }
-    }
+    ForwardPostOrderTraversal Traversal(*this, From);
+    SmallVector<BasicBlock *, 16> PostOrder(Traversal.begin(), Traversal.end());
 
     // 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

>From 3e70e3dbccfe63157996d09882769772e351ff4c Mon Sep 17 00:00:00 2001
From: Arseniy Obolenskiy <arseniy.obolenskiy at amd.com>
Date: Thu, 30 Jul 2026 15:58:25 +0200
Subject: [PATCH 3/3] simplify comments

---
 .../SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp    | 9 +++------
 .../Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp | 7 +++----
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
index 3092ddb764c67..b782c98a4a01d 100644
--- a/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
+++ b/llvm/lib/Target/SPIRV/Analysis/SPIRVConvergenceRegionAnalysis.cpp
@@ -239,10 +239,8 @@ class ConvergenceRegionAnalyzer {
     ForwardPostOrderTraversal Traversal(*this, From);
     SmallVector<BasicBlock *, 16> PostOrder(Traversal.begin(), Traversal.end());
 
-    // 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.
+    // |PostOrder| puts successors before predecessors, so reachability to a
+    // match propagates in one linear scan instead of per-path recursion.
     SmallPtrSet<BasicBlock *, 0> Output;
     for (auto *BB : PostOrder) {
       bool ReachesMatch = false;
@@ -256,8 +254,7 @@ class ConvergenceRegionAnalyzer {
       if (isMatch(BB) || ReachesMatch)
         Output.insert(BB);
 
-      // Preserve whole-loop inclusion: a qualifying path crossing a loop
-      // header brings in the entire loop.
+      // A qualifying path through a loop header brings in the whole loop.
       if (ReachesMatch && LI.isLoopHeader(BB)) {
         auto *L = LI.getLoopFor(BB);
         for (auto *LoopBB : L->getBlocks())
diff --git a/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp b/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
index 32e0589440696..faf3cc1f74abb 100644
--- a/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
+++ b/llvm/unittests/Target/SPIRV/SPIRVConvergenceRegionAnalysisTests.cpp
@@ -954,10 +954,9 @@ 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.
+// Regression test for findPathsToMatch: a naive recursive enumeration
+// revisits the common suffix once per incoming path, growing exponentially
+// with the number of diamonds.
 TEST_F(SPIRVConvergenceRegionAnalysisTest,
        ReconvergingDiamondsWithConvergenceBranch) {
   StringRef Assembly = R"(



More information about the llvm-commits mailing list