[llvm] [Transforms] Add DeadBranchElimination pass (PR #211176)
Raz Luvaton via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 23 10:47:48 PDT 2026
https://github.com/rluvaton updated https://github.com/llvm/llvm-project/pull/211176
>From 5907d82469d4907493d9f8333c5b62ff3880c8ab Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:15:28 +0300
Subject: [PATCH 01/13] [Transforms] Add DeadBranchElimination pass
Eliminates conditional branches that are unreachable but cannot be
proven unreachable directly because the branch body modifies the values
the condition depends on (llvm/llvm-project#160001):
int a = 0, b = 0, limit = 100;
while (a < limit) {
if (b == limit) // unreachable, but modifies limit
limit += 1;
a++; b++;
}
The pass runs an optimistic fixed point over both bodies of every
conditional branch: assume all bodies dead, replace them with
'unreachable' in a function clone, run ScalarEvolution on the cleaned
clone, and restore every body whose branch edge cannot be proven
never-taken. At convergence the remaining assumption set is
self-consistent and those branches are folded.
Scheduled in buildModuleSimplificationPipeline after GlobalOpt and
before the GlobalCleanup SimplifyCFG, which would otherwise speculate
such branches into selects (data dependencies) that CFG reasoning can
no longer remove. Gated by -enable-dead-branch-elim (default on).
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Transforms/Scalar/DeadBranchElimination.h | 35 +++
llvm/lib/Passes/PassBuilder.cpp | 1 +
llvm/lib/Passes/PassBuilderPipelines.cpp | 13 +
llvm/lib/Passes/PassRegistry.def | 1 +
llvm/lib/Transforms/Scalar/CMakeLists.txt | 1 +
.../Scalar/DeadBranchElimination.cpp | 238 ++++++++++++++++++
llvm/test/Other/new-pm-defaults.ll | 1 +
.../Other/new-pm-thinlto-postlink-defaults.ll | 1 +
.../new-pm-thinlto-postlink-pgo-defaults.ll | 1 +
...-pm-thinlto-postlink-samplepgo-defaults.ll | 1 +
.../Other/new-pm-thinlto-prelink-defaults.ll | 1 +
.../new-pm-thinlto-prelink-pgo-defaults.ll | 1 +
...w-pm-thinlto-prelink-samplepgo-defaults.ll | 1 +
.../circular-dependency.ll | 74 ++++++
.../DeadBranchElimination/loop-safety.ll | 66 +++++
.../multi-block-and-cascade.ll | 94 +++++++
.../DeadBranchElimination/phi-merge.ll | 72 ++++++
.../DeadBranchElimination/while-loop.ll | 44 ++++
.../PhaseOrdering/dead-branch-elim.ll | 47 ++++
19 files changed, 693 insertions(+)
create mode 100644 llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
create mode 100644 llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
create mode 100644 llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
create mode 100644 llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
create mode 100644 llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
create mode 100644 llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
create mode 100644 llvm/test/Transforms/DeadBranchElimination/while-loop.ll
create mode 100644 llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
diff --git a/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h b/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
new file mode 100644
index 0000000000000..19fdcc06035bf
--- /dev/null
+++ b/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
@@ -0,0 +1,35 @@
+//===- DeadBranchElimination.h ----------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Eliminates conditional branches that are unreachable, but that cannot be
+// proven unreachable directly because the branch body modifies the values the
+// condition depends on (a circular dependency). Uses an optimistic fixed
+// point: assume all branch bodies dead, re-run ScalarEvolution on a clone
+// with those bodies removed, and restore every body whose branch edge cannot
+// be proven never-taken, until the assumption set is self-consistent.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_TRANSFORMS_SCALAR_DEADBRANCHELIMINATION_H
+#define LLVM_TRANSFORMS_SCALAR_DEADBRANCHELIMINATION_H
+
+#include "llvm/IR/PassManager.h"
+
+namespace llvm {
+
+class Module;
+
+class DeadBranchEliminationPass
+ : public PassInfoMixin<DeadBranchEliminationPass> {
+public:
+ PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+};
+
+} // namespace llvm
+
+#endif // LLVM_TRANSFORMS_SCALAR_DEADBRANCHELIMINATION_H
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 17bc2a5a5afdd..276b2e9e7201c 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -291,6 +291,7 @@
#include "llvm/Transforms/Scalar/CorrelatedValuePropagation.h"
#include "llvm/Transforms/Scalar/DCE.h"
#include "llvm/Transforms/Scalar/DFAJumpThreading.h"
+#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
#include "llvm/Transforms/Scalar/DeadStoreElimination.h"
#include "llvm/Transforms/Scalar/DivRemPairs.h"
#include "llvm/Transforms/Scalar/DropUnnecessaryAssumes.h"
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index d62828c78bfe4..26c31086e39c5 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -95,6 +95,7 @@
#include "llvm/Transforms/Scalar/ConstraintElimination.h"
#include "llvm/Transforms/Scalar/CorrelatedValuePropagation.h"
#include "llvm/Transforms/Scalar/DFAJumpThreading.h"
+#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
#include "llvm/Transforms/Scalar/DeadStoreElimination.h"
#include "llvm/Transforms/Scalar/DivRemPairs.h"
#include "llvm/Transforms/Scalar/DropUnnecessaryAssumes.h"
@@ -207,6 +208,10 @@ static cl::opt<bool> EnableGlobalAnalyses(
"enable-global-analyses", cl::init(true), cl::Hidden,
cl::desc("Enable inter-procedural analyses"));
+static cl::opt<bool> EnableDeadBranchElimination(
+ "enable-dead-branch-elim", cl::init(true), cl::Hidden,
+ cl::desc("Enable dead branch elimination (circular-dependency branches)"));
+
static cl::opt<bool> RunPartialInlining("enable-partial-inlining",
cl::init(false), cl::Hidden,
cl::desc("Run Partial inlining pass"));
@@ -1231,6 +1236,14 @@ PassBuilder::buildModuleSimplificationPipeline(OptimizationLevel Level,
// Optimize globals to try and fold them into constants.
MPM.addPass(GlobalOptPass());
+ // Remove branches that are provably dead only under the assumption that
+ // they are dead (circular dependencies). This must run while the CFG still
+ // reflects the source control flow: the SimplifyCFG below speculates
+ // branch bodies into selects, turning such branches into data dependencies
+ // that can no longer be removed by CFG reasoning.
+ if (EnableDeadBranchElimination)
+ MPM.addPass(DeadBranchEliminationPass());
+
// Create a small function pass pipeline to cleanup after all the global
// optimizations.
FunctionPassManager GlobalCleanupPM;
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index bb5814f377f6b..9473fb92f33d2 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -74,6 +74,7 @@ MODULE_PASS("ctx-prof-flatten",
MODULE_PASS("ctx-prof-flatten-prethinlink",
PGOCtxProfFlatteningPass(/*IsPreThinlink=*/true))
MODULE_PASS("noinline-nonprevailing", NoinlineNonPrevailing())
+MODULE_PASS("dead-branch-elim", DeadBranchEliminationPass())
MODULE_PASS("deadargelim", DeadArgumentEliminationPass())
MODULE_PASS("debugify", NewPMDebugifyPass())
MODULE_PASS("declare-runtime-libcalls", DeclareRuntimeLibcallsPass())
diff --git a/llvm/lib/Transforms/Scalar/CMakeLists.txt b/llvm/lib/Transforms/Scalar/CMakeLists.txt
index c92fd202af968..c6e0af1f65a61 100644
--- a/llvm/lib/Transforms/Scalar/CMakeLists.txt
+++ b/llvm/lib/Transforms/Scalar/CMakeLists.txt
@@ -8,6 +8,7 @@ add_llvm_component_library(LLVMScalarOpts
ConstraintElimination.cpp
CorrelatedValuePropagation.cpp
DCE.cpp
+ DeadBranchElimination.cpp
DeadStoreElimination.cpp
DFAJumpThreading.cpp
DivRemPairs.cpp
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
new file mode 100644
index 0000000000000..e719bd0f429f9
--- /dev/null
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -0,0 +1,238 @@
+//===- DeadBranchElimination.cpp ------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This pass eliminates conditional branches that are unreachable but that
+// cannot be proven unreachable directly, because the branch body modifies the
+// very values its condition depends on. The motivating example:
+//
+// int a = 0, b = 0, limit = 100;
+// while (a < limit) {
+// if (b == limit) // Unreachable: a == b always, and a < limit here.
+// limit += 1; // ...but it modifies limit (circular dependency).
+// a++; b++;
+// }
+//
+// Proving the branch dead requires knowing limit is loop-invariant, which
+// requires knowing the branch is dead.
+//
+// The algorithm is an optimistic fixed point over the two bodies (true side,
+// false side) of every conditional branch. Each body starts as Unknown
+// ("assumed dead") and can only be promoted to ProvenReachable:
+//
+// 1. Clone the function. In the clone, replace every Unknown body with
+// 'unreachable' (redirect the branch edge). Nested branches inside a
+// removed body disappear with it. PHI nodes at merge points drop the
+// incoming values of removed edges, so the bodies' effects vanish.
+// 2. Run ScalarEvolution on the cleaned clone.
+// 3. For each Unknown body, check whether the clone proves its branch edge
+// is never taken (context-sensitive SCEV proof, or the branch sits in
+// unreachable code). If the edge cannot be proven dead, mark it
+// ProvenReachable; its body is restored in the next iteration.
+// 4. Repeat until no status changes. Statuses move in one direction only,
+// so this terminates.
+//
+// At convergence the remaining Unknown set is self-consistent: assuming
+// those bodies never run, the analysis proves they indeed never run
+// (consider the first time one would run -- the clone models the program
+// state exactly up to that point and proves the edge is not taken). Those
+// branches are then folded in the original function.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
+#include "llvm/ADT/DepthFirstIterator.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Analysis/ScalarEvolution.h"
+#include "llvm/IR/CFG.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Support/Debug.h"
+#include "llvm/Transforms/Utils/BasicBlockUtils.h"
+#include "llvm/Transforms/Utils/Cloning.h"
+#include "llvm/Transforms/Utils/Local.h"
+
+using namespace llvm;
+
+#define DEBUG_TYPE "dead-branch-elim"
+
+namespace {
+
+enum class Status { Unknown, ProvenReachable };
+
+/// One side (body) of a conditional branch.
+struct BranchBody {
+ BasicBlock *BranchBB; ///< Block whose terminator is the conditional branch.
+ unsigned SuccIdx; ///< Which side (0 = true body, 1 = false body).
+ Status St = Status::Unknown;
+};
+
+std::vector<BranchBody> collectBranchBodies(Function &F) {
+ std::vector<BranchBody> Bodies;
+ for (BasicBlock &BB : F) {
+ auto *BI = dyn_cast<BranchInst>(BB.getTerminator());
+ if (!BI || !BI->isConditional())
+ continue;
+ if (BI->getSuccessor(0) == BI->getSuccessor(1))
+ continue;
+ Bodies.push_back({&BB, 0});
+ Bodies.push_back({&BB, 1});
+ }
+ return Bodies;
+}
+
+/// Does the clone prove that this branch edge is never taken? The bodies of
+/// all Unknown branches have already been replaced with 'unreachable', so
+/// the condition is evaluated on the cleaned-up code.
+bool isEdgeProvenDead(ScalarEvolution &SE, BranchInst *BI, unsigned SuccIdx) {
+ Value *Cond = BI->getCondition();
+ if (auto *CI = dyn_cast<ConstantInt>(Cond))
+ return CI->isOne() ? SuccIdx == 1 : SuccIdx == 0;
+ if (auto *Cmp = dyn_cast<ICmpInst>(Cond)) {
+ const SCEV *L = SE.getSCEV(Cmp->getOperand(0));
+ const SCEV *R = SE.getSCEV(Cmp->getOperand(1));
+ // The true side is dead when the condition is provably always false, the
+ // false side when it is provably always true. The proof may use
+ // conditions of dominating branches (e.g. the loop guard in an unrotated
+ // while loop), so anchor the query at the branch itself.
+ ICmpInst::Predicate P =
+ SuccIdx == 0 ? Cmp->getInversePredicate() : Cmp->getPredicate();
+ return SE.isKnownPredicateAt(P, L, R, BI);
+ }
+ return false;
+}
+
+/// One fixed-point iteration: rebuild the clone with all Unknown bodies
+/// replaced by 'unreachable', re-run the analysis, and promote every body
+/// whose edge cannot be proven dead. Returns true if any status changed.
+bool refineOnce(Function &F, std::vector<BranchBody> &Bodies,
+ FunctionAnalysisManager &FAM) {
+ ValueToValueMapTy VMap;
+ Function *Clone = CloneFunction(&F, VMap);
+ LLVMContext &Ctx = F.getContext();
+
+ // Replace every Unknown body with 'unreachable': redirect the branch edge
+ // to a trap block. The condition itself is never touched.
+ BasicBlock *TrapBB = nullptr;
+ for (BranchBody &B : Bodies) {
+ if (B.St != Status::Unknown)
+ continue;
+ auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
+ auto *BI = cast<BranchInst>(BB->getTerminator());
+ if (!TrapBB) {
+ TrapBB = BasicBlock::Create(Ctx, "dbe.unreachable", Clone);
+ new UnreachableInst(Ctx, TrapBB);
+ }
+ BasicBlock *Succ = BI->getSuccessor(B.SuccIdx);
+ BI->setSuccessor(B.SuccIdx, TrapBB);
+ Succ->removePredecessor(BB);
+ }
+
+ // A branch nested inside a removed body is itself unreachable in the
+ // clone; it stays Unknown and is removed together with its parent.
+ SmallPtrSet<BasicBlock *, 32> Reachable;
+ for (BasicBlock *BB : depth_first(&Clone->getEntryBlock()))
+ Reachable.insert(BB);
+ std::vector<std::pair<BranchBody *, BasicBlock *>> ToCheck;
+ for (BranchBody &B : Bodies)
+ if (B.St == Status::Unknown) {
+ auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
+ if (Reachable.contains(BB))
+ ToCheck.push_back({&B, BB});
+ }
+
+ // Delete the unreachable blocks so PHI nodes in live blocks drop their
+ // dead incoming values. DeleteDeadBlocks (unlike removeUnreachableBlocks)
+ // never rewrites live terminators, so the branches under test survive.
+ std::vector<BasicBlock *> DeadBlocks;
+ for (BasicBlock &BB : *Clone)
+ if (!Reachable.contains(&BB))
+ DeadBlocks.push_back(&BB);
+ DeleteDeadBlocks(DeadBlocks);
+
+ bool Changed = false;
+ {
+ auto &SE = FAM.getResult<ScalarEvolutionAnalysis>(*Clone);
+ for (auto &[B, BB] : ToCheck) {
+ auto *BI = dyn_cast<BranchInst>(BB->getTerminator());
+ if (!BI || !BI->isConditional()) {
+ // Something rewrote the branch under test; assume reachable.
+ B->St = Status::ProvenReachable;
+ Changed = true;
+ continue;
+ }
+ if (!isEdgeProvenDead(SE, BI, B->SuccIdx)) {
+ B->St = Status::ProvenReachable;
+ Changed = true;
+ }
+ }
+ }
+
+ FAM.clear(*Clone, Clone->getName());
+ Clone->eraseFromParent();
+ return Changed;
+}
+
+/// Redirect each dead branch to its other side, then delete whatever became
+/// unreachable. Returns true if anything changed.
+bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead) {
+ bool Changed = false;
+ for (const BranchBody &B : Dead) {
+ auto *BI = dyn_cast<BranchInst>(B.BranchBB->getTerminator());
+ if (!BI || !BI->isConditional())
+ continue; // Already folded together with a parent body.
+ LLVM_DEBUG(dbgs() << "DBE: folding dead edge " << B.BranchBB->getName()
+ << " -> "
+ << BI->getSuccessor(B.SuccIdx)->getName() << " in "
+ << F.getName() << "\n");
+ Value *Cond = BI->getCondition();
+ BI->setCondition(ConstantInt::getBool(F.getContext(), B.SuccIdx == 1));
+ ConstantFoldTerminator(B.BranchBB);
+ RecursivelyDeleteTriviallyDeadInstructions(Cond);
+ Changed = true;
+ }
+ if (Changed)
+ removeUnreachableBlocks(F);
+ return Changed;
+}
+
+bool runOnFunction(Function &F, FunctionAnalysisManager &FAM) {
+ std::vector<BranchBody> Bodies = collectBranchBodies(F);
+ if (Bodies.empty())
+ return false;
+
+ while (refineOnce(F, Bodies, FAM))
+ ;
+
+ std::vector<BranchBody> Dead;
+ for (BranchBody &B : Bodies)
+ if (B.St == Status::Unknown)
+ Dead.push_back(B);
+ if (Dead.empty())
+ return false;
+
+ return foldDeadBranches(F, Dead);
+}
+
+} // namespace
+
+PreservedAnalyses DeadBranchEliminationPass::run(Module &M,
+ ModuleAnalysisManager &AM) {
+ auto &FAM = AM.getResult<FunctionAnalysisManagerModuleProxy>(M).getManager();
+ bool Changed = false;
+ // This is a module pass (it temporarily creates function clones), but it
+ // transforms functions independently.
+ for (Function &F : M) {
+ if (F.isDeclaration() || F.isPresplitCoroutine() || F.hasOptNone())
+ continue;
+ if (runOnFunction(F, FAM)) {
+ FAM.invalidate(F, PreservedAnalyses::none());
+ Changed = true;
+ }
+ }
+ return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
+}
diff --git a/llvm/test/Other/new-pm-defaults.ll b/llvm/test/Other/new-pm-defaults.ll
index c3a8ab3725ad6..378f6db4d2a74 100644
--- a/llvm/test/Other/new-pm-defaults.ll
+++ b/llvm/test/Other/new-pm-defaults.ll
@@ -107,6 +107,7 @@
; CHECK-O-NEXT: Running pass: IPSCCPPass
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
index df712ab7846f8..cfedb970d5bbe 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
@@ -45,6 +45,7 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
index aa0b2864eaab8..5c5814bd0bb4c 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
@@ -30,6 +30,7 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
index e9d64c4a43163..6b2f942bc6508 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
@@ -37,6 +37,7 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
diff --git a/llvm/test/Other/new-pm-thinlto-prelink-defaults.ll b/llvm/test/Other/new-pm-thinlto-prelink-defaults.ll
index 335d56b574227..626888e540de5 100644
--- a/llvm/test/Other/new-pm-thinlto-prelink-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-prelink-defaults.ll
@@ -72,6 +72,7 @@
; CHECK-O-NEXT: Running pass: IPSCCPPass
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Other/new-pm-thinlto-prelink-pgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-prelink-pgo-defaults.ll
index b60a8af7be33e..c90e321bce0f6 100644
--- a/llvm/test/Other/new-pm-thinlto-prelink-pgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-prelink-pgo-defaults.ll
@@ -40,6 +40,7 @@
; CHECK-O-NEXT: Running pass: IPSCCPPass
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Other/new-pm-thinlto-prelink-samplepgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-prelink-samplepgo-defaults.ll
index 34ff5ae50f30b..de786eae67ff2 100644
--- a/llvm/test/Other/new-pm-thinlto-prelink-samplepgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-prelink-samplepgo-defaults.ll
@@ -43,6 +43,7 @@
; CHECK-O-NEXT: Running pass: IPSCCPPass
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll b/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
new file mode 100644
index 0000000000000..2d8a459aa1074
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
@@ -0,0 +1,74 @@
+; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
+
+; The motivating example (rotated loop form):
+; int a = 0, b = 0, limit = 100;
+; while (a < limit) {
+; if (b == limit) // unreachable: a == b always, and a < limit here
+; limit += 1; // ...but it modifies limit (circular dependency)
+; a++; b++;
+; }
+; The 'loop' -> 'if.then' edge must be removed. @keep (whose branch is
+; genuinely taken) must stay intact.
+
+; CHECK-LABEL: define i32 @run()
+; CHECK: loop:
+; CHECK-NOT: if.then:
+; CHECK-NOT: icmp eq
+; CHECK: br label %latch
+; CHECK: latch:
+; CHECK: br i1 %cmp.outer, label %loop, label %exit
+; CHECK: exit:
+define i32 @run() {
+entry:
+ br label %loop
+
+loop:
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
+ %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
+ %cmp.inner = icmp eq i32 %b, %limit
+ br i1 %cmp.inner, label %if.then, label %latch
+
+if.then:
+ %limit.inc = add nsw i32 %limit, 1
+ br label %latch
+
+latch:
+ %limit.next = phi i32 [ %limit.inc, %if.then ], [ %limit, %loop ]
+ %a.next = add nsw i32 %a, 1
+ %b.next = add nsw i32 %b, 1
+ %cmp.outer = icmp slt i32 %a.next, %limit.next
+ br i1 %cmp.outer, label %loop, label %exit
+
+exit:
+ ret i32 %b.next
+}
+
+; CHECK-LABEL: define i32 @keep()
+; CHECK: loop:
+; CHECK: br i1 %cmp.inner, label %if.then, label %latch
+; CHECK: if.then:
+; CHECK: latch:
+define i32 @keep() {
+entry:
+ br label %loop
+
+loop:
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %sum = phi i32 [ 0, %entry ], [ %sum.next, %latch ]
+ %cmp.inner = icmp eq i32 %a, 50
+ br i1 %cmp.inner, label %if.then, label %latch
+
+if.then:
+ %sum.inc = add nsw i32 %sum, %a
+ br label %latch
+
+latch:
+ %sum.next = phi i32 [ %sum.inc, %if.then ], [ %sum, %loop ]
+ %a.next = add nsw i32 %a, 1
+ %cmp.outer = icmp slt i32 %a.next, 100
+ br i1 %cmp.outer, label %loop, label %exit
+
+exit:
+ ret i32 %sum.next
+}
diff --git a/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll b/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
new file mode 100644
index 0000000000000..ff87e47340cb7
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
@@ -0,0 +1,66 @@
+; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
+
+; Loop control flow must never be touched: exit edges of counted loops and
+; runtime-bound loops are "not provable" in early iterations of the fixed
+; point but must end up ProvenReachable, otherwise loops become infinite.
+
+; CHECK-LABEL: define void @counted_loop()
+; CHECK: loop:
+; CHECK: br i1 %cmp, label %loop, label %exit
+; CHECK: exit:
+define void @counted_loop() {
+entry:
+ br label %loop
+
+loop:
+ %i = phi i32 [ 0, %entry ], [ %next, %loop ]
+ %next = add i32 %i, 1
+ %cmp = icmp ult i32 %next, 100
+ br i1 %cmp, label %loop, label %exit
+
+exit:
+ ret void
+}
+
+; CHECK-LABEL: define i32 @runtime_bound(i32 %n)
+; CHECK: loop:
+; CHECK: br i1 %cmp, label %body, label %exit
+; CHECK: body:
+; CHECK: exit:
+define i32 @runtime_bound(i32 %n) {
+entry:
+ br label %loop
+
+loop:
+ %i = phi i32 [ 0, %entry ], [ %i.next, %body ]
+ %sum = phi i32 [ 0, %entry ], [ %sum.next, %body ]
+ %cmp = icmp slt i32 %i, %n
+ br i1 %cmp, label %body, label %exit
+
+body:
+ %sum.next = add nsw i32 %sum, %i
+ %i.next = add nsw i32 %i, 1
+ br label %loop
+
+exit:
+ ret i32 %sum
+}
+
+; Conditions that cannot be analyzed (argument-dependent, memory-based) must
+; keep their branches.
+; CHECK-LABEL: define void @arg_cond(i32 %n)
+; CHECK: br i1 %cmp, label %then, label %done
+; CHECK: then:
+declare void @side_effect()
+define void @arg_cond(i32 %n) {
+entry:
+ %cmp = icmp eq i32 %n, 42
+ br i1 %cmp, label %then, label %done
+
+then:
+ call void @side_effect()
+ br label %done
+
+done:
+ ret void
+}
diff --git a/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll b/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
new file mode 100644
index 0000000000000..a926b6dfd265e
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
@@ -0,0 +1,94 @@
+; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
+; The pass must be idempotent:
+; RUN: opt -passes=dead-branch-elim,dead-branch-elim -S %s | FileCheck %s
+
+; @multi_block: the dead body spans several blocks (a nested if); cutting the
+; branch edge must remove the whole region, including the nested limit
+; modifications.
+; CHECK-LABEL: define i32 @multi_block()
+; CHECK: loop:
+; CHECK-NOT: if.then:
+; CHECK-NOT: deep:
+; CHECK-NOT: if.end:
+; CHECK-NOT: icmp eq
+; CHECK: latch:
+; CHECK: exit:
+define i32 @multi_block() {
+entry:
+ br label %loop
+
+loop:
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
+ %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
+ %cmp.inner = icmp eq i32 %b, %limit
+ br i1 %cmp.inner, label %if.then, label %latch
+
+if.then:
+ %deep.cmp = icmp sgt i32 %a, 5
+ br i1 %deep.cmp, label %deep, label %if.end
+
+deep:
+ %limit.inc2 = add nsw i32 %limit, 2
+ br label %if.end
+
+if.end:
+ %limit.new = phi i32 [ %limit.inc2, %deep ], [ %limit, %if.then ]
+ %limit.inc1 = add nsw i32 %limit.new, 1
+ br label %latch
+
+latch:
+ %limit.next = phi i32 [ %limit.inc1, %if.end ], [ %limit, %loop ]
+ %a.next = add nsw i32 %a, 1
+ %b.next = add nsw i32 %b, 1
+ %cmp.outer = icmp slt i32 %a.next, %limit.next
+ br i1 %cmp.outer, label %loop, label %exit
+
+exit:
+ ret i32 %b.next
+}
+
+; @cascade: 'tail' bumps limit and is reachable from two dead sources.
+; Neither is provable in isolation; only the fixed point that assumes both
+; dead at once can remove them.
+; CHECK-LABEL: define i32 @cascade()
+; CHECK: loop:
+; CHECK-NOT: body1:
+; CHECK-NOT: tail:
+; CHECK-NOT: icmp sgt
+; CHECK-NOT: icmp eq
+; CHECK: mid:
+; CHECK: latch:
+; CHECK: exit:
+define i32 @cascade() {
+entry:
+ br label %loop
+
+loop:
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
+ %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
+ %cmp1 = icmp sgt i32 %a, 1000
+ br i1 %cmp1, label %body1, label %mid
+
+body1:
+ br label %tail
+
+mid:
+ %cmp2 = icmp eq i32 %b, %limit
+ br i1 %cmp2, label %tail, label %latch
+
+tail:
+ %limit.bump = add nsw i32 %limit, 1
+ br label %latch
+
+latch:
+ %limit.next = phi i32 [ %limit.bump, %tail ], [ %limit, %mid ]
+ %a.next = add nsw i32 %a, 1
+ %b.next = add nsw i32 %b, 1
+ %cmp.out = icmp slt i32 %a.next, 100
+ br i1 %cmp.out, label %loop, label %exit
+
+exit:
+ ret i32 %limit.next
+}
diff --git a/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll b/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
new file mode 100644
index 0000000000000..62a7727bdec18
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
@@ -0,0 +1,72 @@
+; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
+
+; PHI nodes at the merge point must be rewired to the surviving side's value,
+; in both fold directions.
+
+; CHECK-LABEL: define i32 @then_side_dead()
+; CHECK-NOT: then:
+; CHECK: else:
+; CHECK: %b = add nsw i32 %x, 1
+; CHECK-NOT: %x.next = phi
+; CHECK: ret i32 %b
+define i32 @then_side_dead() {
+entry:
+ br label %loop
+
+loop:
+ %i = phi i32 [ 0, %entry ], [ %i.next, %merge ]
+ %x = phi i32 [ 0, %entry ], [ %x.next, %merge ]
+ %cmp = icmp sgt i32 %i, 1000
+ br i1 %cmp, label %then, label %else
+
+then:
+ %a = add nsw i32 %x, 100
+ br label %merge
+
+else:
+ %b = add nsw i32 %x, 1
+ br label %merge
+
+merge:
+ %x.next = phi i32 [ %a, %then ], [ %b, %else ]
+ %i.next = add nsw i32 %i, 1
+ %cond = icmp slt i32 %i.next, 10
+ br i1 %cond, label %loop, label %exit
+
+exit:
+ ret i32 %x.next
+}
+
+; CHECK-LABEL: define i32 @else_side_dead()
+; CHECK: then:
+; CHECK: %a = add nsw i32 %x, 3
+; CHECK-NOT: else:
+; CHECK-NOT: %x.next = phi
+; CHECK: ret i32 %a
+define i32 @else_side_dead() {
+entry:
+ br label %loop
+
+loop:
+ %i = phi i32 [ 0, %entry ], [ %i.next, %merge ]
+ %x = phi i32 [ 0, %entry ], [ %x.next, %merge ]
+ %cmp = icmp slt i32 %i, 1000
+ br i1 %cmp, label %then, label %else
+
+then:
+ %a = add nsw i32 %x, 3
+ br label %merge
+
+else:
+ %b = add nsw i32 %x, 7
+ br label %merge
+
+merge:
+ %x.next = phi i32 [ %a, %then ], [ %b, %else ]
+ %i.next = add nsw i32 %i, 1
+ %cond = icmp slt i32 %i.next, 10
+ br i1 %cond, label %loop, label %exit
+
+exit:
+ ret i32 %x.next
+}
diff --git a/llvm/test/Transforms/DeadBranchElimination/while-loop.ll b/llvm/test/Transforms/DeadBranchElimination/while-loop.ll
new file mode 100644
index 0000000000000..1dbb742f588f7
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/while-loop.ll
@@ -0,0 +1,44 @@
+; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
+
+; The motivating example exactly as clang -O0 + mem2reg emits it: an
+; UNROTATED while loop, guard in the header, inner check in the body.
+; Proving b != limit needs the dominating guard a < limit (b's global range
+; includes the value reached on the exit iteration), i.e. a context-sensitive
+; SCEV query.
+
+; CHECK-LABEL: define i32 @run()
+; CHECK: header:
+; CHECK: body:
+; CHECK-NOT: if.then:
+; CHECK-NOT: icmp eq
+; CHECK: br label %latch
+; CHECK: latch:
+; CHECK: exit:
+define i32 @run() {
+entry:
+ br label %header
+
+header:
+ %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
+ %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %guard = icmp slt i32 %a, %limit
+ br i1 %guard, label %body, label %exit
+
+body:
+ %cmp.inner = icmp eq i32 %b, %limit
+ br i1 %cmp.inner, label %if.then, label %latch
+
+if.then:
+ %limit.inc = add nsw i32 %limit, 1
+ br label %latch
+
+latch:
+ %limit.next = phi i32 [ %limit.inc, %if.then ], [ %limit, %body ]
+ %a.next = add nsw i32 %a, 1
+ %b.next = add nsw i32 %b, 1
+ br label %header
+
+exit:
+ ret i32 %b
+}
diff --git a/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
new file mode 100644
index 0000000000000..4454f87cb3345
--- /dev/null
+++ b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
@@ -0,0 +1,47 @@
+; RUN: opt -O2 -S %s | FileCheck %s
+; RUN: opt -O2 -enable-dead-branch-elim=false -S %s | FileCheck %s --check-prefix=DISABLED
+
+; With dead-branch-elim in the pipeline the circular-dependency branch is
+; removed before SimplifyCFG can speculate it into a select, and -O2 then
+; collapses the whole loop:
+; int a = 0, b = 0, limit = 100;
+; while (a < limit) {
+; if (b == limit) limit += 1;
+; a++; b++;
+; }
+; return b; // == 100
+
+; CHECK-LABEL: define {{.*}}i32 @run()
+; CHECK-NEXT: entry:
+; CHECK-NEXT: ret i32 100
+
+; DISABLED-LABEL: define {{.*}}i32 @run()
+; DISABLED: icmp eq
+define i32 @run() {
+entry:
+ br label %header
+
+header:
+ %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
+ %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
+ %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
+ %guard = icmp slt i32 %a, %limit
+ br i1 %guard, label %body, label %exit
+
+body:
+ %cmp.inner = icmp eq i32 %b, %limit
+ br i1 %cmp.inner, label %if.then, label %latch
+
+if.then:
+ %limit.inc = add nsw i32 %limit, 1
+ br label %latch
+
+latch:
+ %limit.next = phi i32 [ %limit.inc, %if.then ], [ %limit, %body ]
+ %a.next = add nsw i32 %a, 1
+ %b.next = add nsw i32 %b, 1
+ br label %header
+
+exit:
+ ret i32 %b
+}
>From 9fac5e01ae7657918a355954581ac91d37ad092e Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:16:08 +0300
Subject: [PATCH 02/13] [DBE] Analyze clones with a private analysis manager
Using the pipeline's instrumented FunctionAnalysisManager for the
short-lived clones leaks them into -debug-pass-manager logs and risks
stale cache hits when a later clone reuses a freed Function address.
Register just the analyses ScalarEvolution needs in a private manager
whose lifetime matches the clone. Also stop cloning entirely once no
body is still Unknown.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Scalar/DeadBranchElimination.cpp | 42 ++++++++++++++-----
1 file changed, 32 insertions(+), 10 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index e719bd0f429f9..893bbc6b437b3 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -46,11 +46,18 @@
#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
#include "llvm/ADT/DepthFirstIterator.h"
+#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Analysis/AssumptionCache.h"
+#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/ScalarEvolution.h"
+#include "llvm/Analysis/TargetLibraryInfo.h"
+#include "llvm/Analysis/TargetTransformInfo.h"
#include "llvm/IR/CFG.h"
+#include "llvm/IR/Dominators.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
+#include "llvm/IR/PassInstrumentation.h"
#include "llvm/Support/Debug.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/Transforms/Utils/Cloning.h"
@@ -106,11 +113,26 @@ bool isEdgeProvenDead(ScalarEvolution &SE, BranchInst *BI, unsigned SuccIdx) {
return false;
}
+/// The analyses are computed on short-lived clone functions, so use a
+/// private, uninstrumented analysis manager rather than the surrounding
+/// pipeline's one: the clones must not show up in pass-manager debug logs,
+/// and their cached results must die with them.
+FunctionAnalysisManager makePrivateFAM() {
+ FunctionAnalysisManager FAM;
+ FAM.registerPass([] { return PassInstrumentationAnalysis(); });
+ FAM.registerPass([] { return TargetLibraryAnalysis(); });
+ FAM.registerPass([] { return TargetIRAnalysis(); });
+ FAM.registerPass([] { return AssumptionAnalysis(); });
+ FAM.registerPass([] { return DominatorTreeAnalysis(); });
+ FAM.registerPass([] { return LoopAnalysis(); });
+ FAM.registerPass([] { return ScalarEvolutionAnalysis(); });
+ return FAM;
+}
+
/// One fixed-point iteration: rebuild the clone with all Unknown bodies
/// replaced by 'unreachable', re-run the analysis, and promote every body
/// whose edge cannot be proven dead. Returns true if any status changed.
-bool refineOnce(Function &F, std::vector<BranchBody> &Bodies,
- FunctionAnalysisManager &FAM) {
+bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
ValueToValueMapTy VMap;
Function *Clone = CloneFunction(&F, VMap);
LLVMContext &Ctx = F.getContext();
@@ -156,6 +178,7 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies,
bool Changed = false;
{
+ FunctionAnalysisManager FAM = makePrivateFAM();
auto &SE = FAM.getResult<ScalarEvolutionAnalysis>(*Clone);
for (auto &[B, BB] : ToCheck) {
auto *BI = dyn_cast<BranchInst>(BB->getTerminator());
@@ -172,7 +195,6 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies,
}
}
- FAM.clear(*Clone, Clone->getName());
Clone->eraseFromParent();
return Changed;
}
@@ -200,12 +222,16 @@ bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead) {
return Changed;
}
-bool runOnFunction(Function &F, FunctionAnalysisManager &FAM) {
+bool runOnFunction(Function &F) {
std::vector<BranchBody> Bodies = collectBranchBodies(F);
if (Bodies.empty())
return false;
- while (refineOnce(F, Bodies, FAM))
+ auto HasUnknown = [&] {
+ return any_of(Bodies,
+ [](const BranchBody &B) { return B.St == Status::Unknown; });
+ };
+ while (HasUnknown() && refineOnce(F, Bodies))
;
std::vector<BranchBody> Dead;
@@ -222,17 +248,13 @@ bool runOnFunction(Function &F, FunctionAnalysisManager &FAM) {
PreservedAnalyses DeadBranchEliminationPass::run(Module &M,
ModuleAnalysisManager &AM) {
- auto &FAM = AM.getResult<FunctionAnalysisManagerModuleProxy>(M).getManager();
bool Changed = false;
// This is a module pass (it temporarily creates function clones), but it
// transforms functions independently.
for (Function &F : M) {
if (F.isDeclaration() || F.isPresplitCoroutine() || F.hasOptNone())
continue;
- if (runOnFunction(F, FAM)) {
- FAM.invalidate(F, PreservedAnalyses::none());
- Changed = true;
- }
+ Changed |= runOnFunction(F);
}
return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
>From d74dabff69b2527ce8c88ce39b9231422fcb1cac Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:16:30 +0300
Subject: [PATCH 03/13] [DBE] Use CondBrInst instead of deprecated BranchInst
queries
BranchInst::isConditional and friends are deprecated in favor of the
UncondBrInst/CondBrInst class split.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Transforms/Scalar/DeadBranchElimination.cpp | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index 893bbc6b437b3..75a797c25b70c 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -81,8 +81,8 @@ struct BranchBody {
std::vector<BranchBody> collectBranchBodies(Function &F) {
std::vector<BranchBody> Bodies;
for (BasicBlock &BB : F) {
- auto *BI = dyn_cast<BranchInst>(BB.getTerminator());
- if (!BI || !BI->isConditional())
+ auto *BI = dyn_cast<CondBrInst>(BB.getTerminator());
+ if (!BI)
continue;
if (BI->getSuccessor(0) == BI->getSuccessor(1))
continue;
@@ -95,7 +95,7 @@ std::vector<BranchBody> collectBranchBodies(Function &F) {
/// Does the clone prove that this branch edge is never taken? The bodies of
/// all Unknown branches have already been replaced with 'unreachable', so
/// the condition is evaluated on the cleaned-up code.
-bool isEdgeProvenDead(ScalarEvolution &SE, BranchInst *BI, unsigned SuccIdx) {
+bool isEdgeProvenDead(ScalarEvolution &SE, CondBrInst *BI, unsigned SuccIdx) {
Value *Cond = BI->getCondition();
if (auto *CI = dyn_cast<ConstantInt>(Cond))
return CI->isOne() ? SuccIdx == 1 : SuccIdx == 0;
@@ -144,7 +144,7 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
if (B.St != Status::Unknown)
continue;
auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
- auto *BI = cast<BranchInst>(BB->getTerminator());
+ auto *BI = cast<CondBrInst>(BB->getTerminator());
if (!TrapBB) {
TrapBB = BasicBlock::Create(Ctx, "dbe.unreachable", Clone);
new UnreachableInst(Ctx, TrapBB);
@@ -181,8 +181,8 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
FunctionAnalysisManager FAM = makePrivateFAM();
auto &SE = FAM.getResult<ScalarEvolutionAnalysis>(*Clone);
for (auto &[B, BB] : ToCheck) {
- auto *BI = dyn_cast<BranchInst>(BB->getTerminator());
- if (!BI || !BI->isConditional()) {
+ auto *BI = dyn_cast<CondBrInst>(BB->getTerminator());
+ if (!BI) {
// Something rewrote the branch under test; assume reachable.
B->St = Status::ProvenReachable;
Changed = true;
@@ -204,8 +204,8 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead) {
bool Changed = false;
for (const BranchBody &B : Dead) {
- auto *BI = dyn_cast<BranchInst>(B.BranchBB->getTerminator());
- if (!BI || !BI->isConditional())
+ auto *BI = dyn_cast<CondBrInst>(B.BranchBB->getTerminator());
+ if (!BI)
continue; // Already folded together with a parent body.
LLVM_DEBUG(dbgs() << "DBE: folding dead edge " << B.BranchBB->getName()
<< " -> "
>From 36bcbb725e83ae617ecd3bae304bf36ce3af6546 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:17:27 +0300
Subject: [PATCH 04/13] [DBE] Avoid cloning functions without analyzable loop
branches
CloneFunction plus a fresh ScalarEvolution per fixed-point iteration is
the pass's dominant cost. Only branches inside loops with ICmp
conditions can exhibit the circular dependencies this pass targets, so
seed everything else ProvenReachable at collection time and skip the
clone entirely when no body starts Unknown. Straight-line dead branches
are already handled by SCCP/SimplifyCFG. A conditional-branch prescan
avoids computing DominatorTree/LoopInfo for functions with no
candidate-shaped branch at all.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Scalar/DeadBranchElimination.cpp | 33 ++++++++++++++++---
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index 75a797c25b70c..8f9bc523f985a 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -78,7 +78,12 @@ struct BranchBody {
Status St = Status::Unknown;
};
-std::vector<BranchBody> collectBranchBodies(Function &F) {
+/// Cloning and re-running ScalarEvolution is only worthwhile for the
+/// pattern this pass targets: a branch inside a loop whose condition SCEV
+/// can reason about. Everything else is seeded ProvenReachable so that
+/// functions without such branches are never cloned at all. Straight-line
+/// provably-dead branches are left to SCCP/SimplifyCFG.
+std::vector<BranchBody> collectBranchBodies(Function &F, LoopInfo &LI) {
std::vector<BranchBody> Bodies;
for (BasicBlock &BB : F) {
auto *BI = dyn_cast<CondBrInst>(BB.getTerminator());
@@ -86,12 +91,27 @@ std::vector<BranchBody> collectBranchBodies(Function &F) {
continue;
if (BI->getSuccessor(0) == BI->getSuccessor(1))
continue;
- Bodies.push_back({&BB, 0});
- Bodies.push_back({&BB, 1});
+ Value *Cond = BI->getCondition();
+ Status St = Status::Unknown;
+ if (!LI.getLoopFor(&BB) || !isa<ICmpInst>(Cond))
+ St = Status::ProvenReachable;
+ Bodies.push_back({&BB, 0, St});
+ Bodies.push_back({&BB, 1, St});
}
return Bodies;
}
+/// Cheap prescan deciding whether collectBranchBodies can find any Unknown
+/// candidate, before paying for a DominatorTree and LoopInfo.
+bool hasCandidateShapedBranch(Function &F) {
+ for (BasicBlock &BB : F)
+ if (auto *BI = dyn_cast<CondBrInst>(BB.getTerminator()))
+ if (BI->getSuccessor(0) != BI->getSuccessor(1) &&
+ isa<ICmpInst>(BI->getCondition()))
+ return true;
+ return false;
+}
+
/// Does the clone prove that this branch edge is never taken? The bodies of
/// all Unknown branches have already been replaced with 'unreachable', so
/// the condition is evaluated on the cleaned-up code.
@@ -223,7 +243,12 @@ bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead) {
}
bool runOnFunction(Function &F) {
- std::vector<BranchBody> Bodies = collectBranchBodies(F);
+ if (!hasCandidateShapedBranch(F))
+ return false;
+
+ DominatorTree DT(F);
+ LoopInfo LI(DT);
+ std::vector<BranchBody> Bodies = collectBranchBodies(F, LI);
if (Bodies.empty())
return false;
>From b306b996a2b9a440ec8e40c0c114c78caf27aaff Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:26:57 +0300
Subject: [PATCH 05/13] [DBE] Remove PHI predecessor entries before redirecting
the edge
BasicBlock::removePredecessor asserts the edge still exists; calling it
after setSuccessor was a silent no-op in no-assert builds and left
stale PHI incoming values (weakening the analysis) for self-loop
backedges the later dead-block cleanup does not touch. Also add
per-iteration LLVM_DEBUG tracing of status changes.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index 8f9bc523f985a..b978178859d1f 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -169,9 +169,10 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
TrapBB = BasicBlock::Create(Ctx, "dbe.unreachable", Clone);
new UnreachableInst(Ctx, TrapBB);
}
+ // Drop the PHI entries while the edge still exists, then redirect it.
BasicBlock *Succ = BI->getSuccessor(B.SuccIdx);
- BI->setSuccessor(B.SuccIdx, TrapBB);
Succ->removePredecessor(BB);
+ BI->setSuccessor(B.SuccIdx, TrapBB);
}
// A branch nested inside a removed body is itself unreachable in the
@@ -211,7 +212,11 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
if (!isEdgeProvenDead(SE, BI, B->SuccIdx)) {
B->St = Status::ProvenReachable;
Changed = true;
- }
+ LLVM_DEBUG(dbgs() << "DBE: promote " << B->BranchBB->getName() << "/"
+ << B->SuccIdx << "\n");
+ } else
+ LLVM_DEBUG(dbgs() << "DBE: still-dead " << B->BranchBB->getName()
+ << "/" << B->SuccIdx << "\n");
}
}
>From b21417e97893ac72719c63f323e219a6bf00508b Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:27:33 +0300
Subject: [PATCH 06/13] [DBE] Use frontend-shaped input in the PhaseOrdering
test
The previous input was already in SSA form, so the pre-SROA SimplifyCFG
speculated the branch into a select before dead-branch-elim ran. Real
frontend output keeps locals in allocas at that point (stores cannot be
speculated), which is exactly the situation the pass placement relies
on. Use the alloca form.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../PhaseOrdering/dead-branch-elim.ll | 56 ++++++++++++-------
1 file changed, 37 insertions(+), 19 deletions(-)
diff --git a/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
index 4454f87cb3345..abc5c89765f49 100644
--- a/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
+++ b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
@@ -10,6 +10,11 @@
; a++; b++;
; }
; return b; // == 100
+;
+; The input is the frontend's alloca form (clang -O0 without optnone): the
+; early SimplifyCFG cannot speculate stores, SROA then promotes the allocas,
+; and dead-branch-elim must catch the branch before the GlobalCleanup
+; SimplifyCFG turns it into a data dependency.
; CHECK-LABEL: define {{.*}}i32 @run()
; CHECK-NEXT: entry:
@@ -19,29 +24,42 @@
; DISABLED: icmp eq
define i32 @run() {
entry:
- br label %header
+ %a = alloca i32
+ %b = alloca i32
+ %limit = alloca i32
+ store i32 0, ptr %a
+ store i32 0, ptr %b
+ store i32 100, ptr %limit
+ br label %while.cond
-header:
- %limit = phi i32 [ 100, %entry ], [ %limit.next, %latch ]
- %b = phi i32 [ 0, %entry ], [ %b.next, %latch ]
- %a = phi i32 [ 0, %entry ], [ %a.next, %latch ]
- %guard = icmp slt i32 %a, %limit
- br i1 %guard, label %body, label %exit
+while.cond:
+ %a.val = load i32, ptr %a
+ %limit.val = load i32, ptr %limit
+ %guard = icmp slt i32 %a.val, %limit.val
+ br i1 %guard, label %while.body, label %while.end
-body:
- %cmp.inner = icmp eq i32 %b, %limit
- br i1 %cmp.inner, label %if.then, label %latch
+while.body:
+ %b.val = load i32, ptr %b
+ %limit.val2 = load i32, ptr %limit
+ %cmp = icmp eq i32 %b.val, %limit.val2
+ br i1 %cmp, label %if.then, label %if.end
if.then:
- %limit.inc = add nsw i32 %limit, 1
- br label %latch
+ %limit.val3 = load i32, ptr %limit
+ %inc = add nsw i32 %limit.val3, 1
+ store i32 %inc, ptr %limit
+ br label %if.end
-latch:
- %limit.next = phi i32 [ %limit.inc, %if.then ], [ %limit, %body ]
- %a.next = add nsw i32 %a, 1
- %b.next = add nsw i32 %b, 1
- br label %header
+if.end:
+ %a.val2 = load i32, ptr %a
+ %a.inc = add nsw i32 %a.val2, 1
+ store i32 %a.inc, ptr %a
+ %b.val2 = load i32, ptr %b
+ %b.inc = add nsw i32 %b.val2, 1
+ store i32 %b.inc, ptr %b
+ br label %while.cond
-exit:
- ret i32 %b
+while.end:
+ %ret = load i32, ptr %b
+ ret i32 %ret
}
>From 88bbdcfe3222c8a729c013b34251a764f7928ac8 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:38:01 +0300
Subject: [PATCH 07/13] [DBE] Fix pass position in thinlto-postlink pipeline
tests
GlobalOpt materializes TargetLibraryAnalysis in the postlink variants,
so the DeadBranchEliminationPass line belongs after that analysis line.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
llvm/test/Other/new-pm-thinlto-postlink-defaults.ll | 2 +-
llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll | 2 +-
llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
index cfedb970d5bbe..6d981e1428542 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
@@ -45,8 +45,8 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
-; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
index 5c5814bd0bb4c..052c441d74bad 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
@@ -30,8 +30,8 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
-; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
index 6b2f942bc6508..bc0447a4640d8 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
@@ -37,8 +37,8 @@
; CHECK-O-NEXT: Running analysis: TargetIRAnalysis
; CHECK-O-NEXT: Running pass: CalledValuePropagationPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
-; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running analysis: TargetLibraryAnalysis
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: PromotePass
; CHECK-O-NEXT: Running pass: InstCombinePass
; CHECK-O-NEXT: Running analysis: LastRunTrackingAnalysis
>From ccd2e1288967f5309b437271cbaca0549ac77106 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 08:28:01 +0300
Subject: [PATCH 08/13] [DBE] Address review items: statistics, remarks,
iteration cap
- STATISTIC counting folded edges and an optimization remark per fold.
- Cap fixed-point iterations (-dbe-max-iterations, default 8); the fold
is only sound at a verified fixed point, so give up on the function
when the cap is hit mid-refinement.
- Use samesign-aware CmpPredicate queries.
- SmallVector instead of std::vector; clang-format.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Scalar/DeadBranchElimination.cpp | 60 +++++++++++++------
1 file changed, 42 insertions(+), 18 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index b978178859d1f..e1d0a74e2bf61 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -48,8 +48,11 @@
#include "llvm/ADT/DepthFirstIterator.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/Statistic.h"
#include "llvm/Analysis/AssumptionCache.h"
#include "llvm/Analysis/LoopInfo.h"
+#include "llvm/Analysis/OptimizationRemarkEmitter.h"
#include "llvm/Analysis/ScalarEvolution.h"
#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/Analysis/TargetTransformInfo.h"
@@ -58,6 +61,7 @@
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/PassInstrumentation.h"
+#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Debug.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/Transforms/Utils/Cloning.h"
@@ -67,6 +71,12 @@ using namespace llvm;
#define DEBUG_TYPE "dead-branch-elim"
+STATISTIC(NumBranchesFolded, "Number of provably dead branch edges folded");
+
+static cl::opt<unsigned> MaxRefineIterations(
+ "dbe-max-iterations", cl::init(8), cl::Hidden,
+ cl::desc("Maximum fixed-point iterations per function before giving up"));
+
namespace {
enum class Status { Unknown, ProvenReachable };
@@ -83,8 +93,8 @@ struct BranchBody {
/// can reason about. Everything else is seeded ProvenReachable so that
/// functions without such branches are never cloned at all. Straight-line
/// provably-dead branches are left to SCCP/SimplifyCFG.
-std::vector<BranchBody> collectBranchBodies(Function &F, LoopInfo &LI) {
- std::vector<BranchBody> Bodies;
+SmallVector<BranchBody> collectBranchBodies(Function &F, LoopInfo &LI) {
+ SmallVector<BranchBody> Bodies;
for (BasicBlock &BB : F) {
auto *BI = dyn_cast<CondBrInst>(BB.getTerminator());
if (!BI)
@@ -126,8 +136,8 @@ bool isEdgeProvenDead(ScalarEvolution &SE, CondBrInst *BI, unsigned SuccIdx) {
// false side when it is provably always true. The proof may use
// conditions of dominating branches (e.g. the loop guard in an unrotated
// while loop), so anchor the query at the branch itself.
- ICmpInst::Predicate P =
- SuccIdx == 0 ? Cmp->getInversePredicate() : Cmp->getPredicate();
+ CmpPredicate P =
+ SuccIdx == 0 ? Cmp->getInverseCmpPredicate() : Cmp->getCmpPredicate();
return SE.isKnownPredicateAt(P, L, R, BI);
}
return false;
@@ -152,7 +162,7 @@ FunctionAnalysisManager makePrivateFAM() {
/// One fixed-point iteration: rebuild the clone with all Unknown bodies
/// replaced by 'unreachable', re-run the analysis, and promote every body
/// whose edge cannot be proven dead. Returns true if any status changed.
-bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
+bool refineOnce(Function &F, MutableArrayRef<BranchBody> Bodies) {
ValueToValueMapTy VMap;
Function *Clone = CloneFunction(&F, VMap);
LLVMContext &Ctx = F.getContext();
@@ -180,7 +190,7 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
SmallPtrSet<BasicBlock *, 32> Reachable;
for (BasicBlock *BB : depth_first(&Clone->getEntryBlock()))
Reachable.insert(BB);
- std::vector<std::pair<BranchBody *, BasicBlock *>> ToCheck;
+ SmallVector<std::pair<BranchBody *, BasicBlock *>> ToCheck;
for (BranchBody &B : Bodies)
if (B.St == Status::Unknown) {
auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
@@ -191,7 +201,7 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
// Delete the unreachable blocks so PHI nodes in live blocks drop their
// dead incoming values. DeleteDeadBlocks (unlike removeUnreachableBlocks)
// never rewrites live terminators, so the branches under test survive.
- std::vector<BasicBlock *> DeadBlocks;
+ SmallVector<BasicBlock *> DeadBlocks;
for (BasicBlock &BB : *Clone)
if (!Reachable.contains(&BB))
DeadBlocks.push_back(&BB);
@@ -215,8 +225,8 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
LLVM_DEBUG(dbgs() << "DBE: promote " << B->BranchBB->getName() << "/"
<< B->SuccIdx << "\n");
} else
- LLVM_DEBUG(dbgs() << "DBE: still-dead " << B->BranchBB->getName()
- << "/" << B->SuccIdx << "\n");
+ LLVM_DEBUG(dbgs() << "DBE: still-dead " << B->BranchBB->getName() << "/"
+ << B->SuccIdx << "\n");
}
}
@@ -226,16 +236,21 @@ bool refineOnce(Function &F, std::vector<BranchBody> &Bodies) {
/// Redirect each dead branch to its other side, then delete whatever became
/// unreachable. Returns true if anything changed.
-bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead) {
+bool foldDeadBranches(Function &F, ArrayRef<BranchBody> Dead,
+ OptimizationRemarkEmitter &ORE) {
bool Changed = false;
for (const BranchBody &B : Dead) {
auto *BI = dyn_cast<CondBrInst>(B.BranchBB->getTerminator());
if (!BI)
continue; // Already folded together with a parent body.
LLVM_DEBUG(dbgs() << "DBE: folding dead edge " << B.BranchBB->getName()
- << " -> "
- << BI->getSuccessor(B.SuccIdx)->getName() << " in "
- << F.getName() << "\n");
+ << " -> " << BI->getSuccessor(B.SuccIdx)->getName()
+ << " in " << F.getName() << "\n");
+ ORE.emit([&]() {
+ return OptimizationRemark(DEBUG_TYPE, "DeadBranchFolded", BI)
+ << "removed branch to provably unreachable code";
+ });
+ ++NumBranchesFolded;
Value *Cond = BI->getCondition();
BI->setCondition(ConstantInt::getBool(F.getContext(), B.SuccIdx == 1));
ConstantFoldTerminator(B.BranchBB);
@@ -253,7 +268,7 @@ bool runOnFunction(Function &F) {
DominatorTree DT(F);
LoopInfo LI(DT);
- std::vector<BranchBody> Bodies = collectBranchBodies(F, LI);
+ SmallVector<BranchBody> Bodies = collectBranchBodies(F, LI);
if (Bodies.empty())
return false;
@@ -261,17 +276,26 @@ bool runOnFunction(Function &F) {
return any_of(Bodies,
[](const BranchBody &B) { return B.St == Status::Unknown; });
};
- while (HasUnknown() && refineOnce(F, Bodies))
- ;
+ unsigned Iterations = 0;
+ bool StatusChanged = true;
+ while (HasUnknown() && StatusChanged) {
+ // The fold below is only sound at a verified fixed point: an iteration
+ // that promoted bodies invalidates the proofs of the remaining Unknown
+ // set. If the cap cuts the loop short, give up on this function.
+ if (++Iterations > MaxRefineIterations)
+ return false;
+ StatusChanged = refineOnce(F, Bodies);
+ }
- std::vector<BranchBody> Dead;
+ SmallVector<BranchBody> Dead;
for (BranchBody &B : Bodies)
if (B.St == Status::Unknown)
Dead.push_back(B);
if (Dead.empty())
return false;
- return foldDeadBranches(F, Dead);
+ OptimizationRemarkEmitter ORE(&F);
+ return foldDeadBranches(F, Dead, ORE);
}
} // namespace
>From fa08c8a514f99f9c9e027777c810ccaf7876ab69 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 08:28:25 +0300
Subject: [PATCH 09/13] [DBE] Autogenerate test assertions with
update_test_checks.py
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../circular-dependency.ll | 49 ++++++++++++-----
.../DeadBranchElimination/loop-safety.ll | 51 +++++++++++++-----
.../multi-block-and-cascade.ll | 52 +++++++++++++------
.../DeadBranchElimination/phi-merge.ll | 47 ++++++++++++-----
.../DeadBranchElimination/while-loop.ll | 27 +++++++---
.../PhaseOrdering/dead-branch-elim.ll | 27 ++++++++--
6 files changed, 186 insertions(+), 67 deletions(-)
diff --git a/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll b/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
index 2d8a459aa1074..1251df6bbd30b 100644
--- a/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
+++ b/llvm/test/Transforms/DeadBranchElimination/circular-dependency.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
; The motivating example (rotated loop form):
@@ -10,15 +11,23 @@
; The 'loop' -> 'if.then' edge must be removed. @keep (whose branch is
; genuinely taken) must stay intact.
-; CHECK-LABEL: define i32 @run()
-; CHECK: loop:
-; CHECK-NOT: if.then:
-; CHECK-NOT: icmp eq
-; CHECK: br label %latch
-; CHECK: latch:
-; CHECK: br i1 %cmp.outer, label %loop, label %exit
-; CHECK: exit:
define i32 @run() {
+; CHECK-LABEL: define i32 @run() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[A:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A_NEXT:%.*]], %[[LATCH:.*]] ]
+; CHECK-NEXT: [[B:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[LIMIT:%.*]] = phi i32 [ 100, %[[ENTRY]] ], [ [[LIMIT]], %[[LATCH]] ]
+; CHECK-NEXT: br label %[[LATCH]]
+; CHECK: [[LATCH]]:
+; CHECK-NEXT: [[A_NEXT]] = add nsw i32 [[A]], 1
+; CHECK-NEXT: [[B_NEXT]] = add nsw i32 [[B]], 1
+; CHECK-NEXT: [[CMP_OUTER:%.*]] = icmp slt i32 [[A_NEXT]], [[LIMIT]]
+; CHECK-NEXT: br i1 [[CMP_OUTER]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[B_NEXT]]
+;
entry:
br label %loop
@@ -44,12 +53,26 @@ exit:
ret i32 %b.next
}
-; CHECK-LABEL: define i32 @keep()
-; CHECK: loop:
-; CHECK: br i1 %cmp.inner, label %if.then, label %latch
-; CHECK: if.then:
-; CHECK: latch:
define i32 @keep() {
+; CHECK-LABEL: define i32 @keep() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[A:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A_NEXT:%.*]], %[[LATCH:.*]] ]
+; CHECK-NEXT: [[SUM:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[SUM_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[CMP_INNER:%.*]] = icmp eq i32 [[A]], 50
+; CHECK-NEXT: br i1 [[CMP_INNER]], label %[[IF_THEN:.*]], label %[[LATCH]]
+; CHECK: [[IF_THEN]]:
+; CHECK-NEXT: [[SUM_INC:%.*]] = add nsw i32 [[SUM]], [[A]]
+; CHECK-NEXT: br label %[[LATCH]]
+; CHECK: [[LATCH]]:
+; CHECK-NEXT: [[SUM_NEXT]] = phi i32 [ [[SUM_INC]], %[[IF_THEN]] ], [ [[SUM]], %[[LOOP]] ]
+; CHECK-NEXT: [[A_NEXT]] = add nsw i32 [[A]], 1
+; CHECK-NEXT: [[CMP_OUTER:%.*]] = icmp slt i32 [[A_NEXT]], 100
+; CHECK-NEXT: br i1 [[CMP_OUTER]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[SUM_NEXT]]
+;
entry:
br label %loop
diff --git a/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll b/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
index ff87e47340cb7..3f2e35a1dd76f 100644
--- a/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
+++ b/llvm/test/Transforms/DeadBranchElimination/loop-safety.ll
@@ -1,14 +1,22 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
; Loop control flow must never be touched: exit edges of counted loops and
; runtime-bound loops are "not provable" in early iterations of the fixed
; point but must end up ProvenReachable, otherwise loops become infinite.
-; CHECK-LABEL: define void @counted_loop()
-; CHECK: loop:
-; CHECK: br i1 %cmp, label %loop, label %exit
-; CHECK: exit:
define void @counted_loop() {
+; CHECK-LABEL: define void @counted_loop() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[I:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[NEXT:%.*]], %[[LOOP]] ]
+; CHECK-NEXT: [[NEXT]] = add i32 [[I]], 1
+; CHECK-NEXT: [[CMP:%.*]] = icmp ult i32 [[NEXT]], 100
+; CHECK-NEXT: br i1 [[CMP]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret void
+;
entry:
br label %loop
@@ -22,12 +30,23 @@ exit:
ret void
}
-; CHECK-LABEL: define i32 @runtime_bound(i32 %n)
-; CHECK: loop:
-; CHECK: br i1 %cmp, label %body, label %exit
-; CHECK: body:
-; CHECK: exit:
define i32 @runtime_bound(i32 %n) {
+; CHECK-LABEL: define i32 @runtime_bound(
+; CHECK-SAME: i32 [[N:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[I:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[I_NEXT:%.*]], %[[BODY:.*]] ]
+; CHECK-NEXT: [[SUM:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[SUM_NEXT:%.*]], %[[BODY]] ]
+; CHECK-NEXT: [[CMP:%.*]] = icmp slt i32 [[I]], [[N]]
+; CHECK-NEXT: br i1 [[CMP]], label %[[BODY]], label %[[EXIT:.*]]
+; CHECK: [[BODY]]:
+; CHECK-NEXT: [[SUM_NEXT]] = add nsw i32 [[SUM]], [[I]]
+; CHECK-NEXT: [[I_NEXT]] = add nsw i32 [[I]], 1
+; CHECK-NEXT: br label %[[LOOP]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[SUM]]
+;
entry:
br label %loop
@@ -48,11 +67,19 @@ exit:
; Conditions that cannot be analyzed (argument-dependent, memory-based) must
; keep their branches.
-; CHECK-LABEL: define void @arg_cond(i32 %n)
-; CHECK: br i1 %cmp, label %then, label %done
-; CHECK: then:
declare void @side_effect()
define void @arg_cond(i32 %n) {
+; CHECK-LABEL: define void @arg_cond(
+; CHECK-SAME: i32 [[N:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[N]], 42
+; CHECK-NEXT: br i1 [[CMP]], label %[[THEN:.*]], label %[[DONE:.*]]
+; CHECK: [[THEN]]:
+; CHECK-NEXT: call void @side_effect()
+; CHECK-NEXT: br label %[[DONE]]
+; CHECK: [[DONE]]:
+; CHECK-NEXT: ret void
+;
entry:
%cmp = icmp eq i32 %n, 42
br i1 %cmp, label %then, label %done
diff --git a/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll b/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
index a926b6dfd265e..fb608330894f8 100644
--- a/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
+++ b/llvm/test/Transforms/DeadBranchElimination/multi-block-and-cascade.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
; The pass must be idempotent:
; RUN: opt -passes=dead-branch-elim,dead-branch-elim -S %s | FileCheck %s
@@ -5,15 +6,23 @@
; @multi_block: the dead body spans several blocks (a nested if); cutting the
; branch edge must remove the whole region, including the nested limit
; modifications.
-; CHECK-LABEL: define i32 @multi_block()
-; CHECK: loop:
-; CHECK-NOT: if.then:
-; CHECK-NOT: deep:
-; CHECK-NOT: if.end:
-; CHECK-NOT: icmp eq
-; CHECK: latch:
-; CHECK: exit:
define i32 @multi_block() {
+; CHECK-LABEL: define i32 @multi_block() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[A:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A_NEXT:%.*]], %[[LATCH:.*]] ]
+; CHECK-NEXT: [[B:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[LIMIT:%.*]] = phi i32 [ 100, %[[ENTRY]] ], [ [[LIMIT]], %[[LATCH]] ]
+; CHECK-NEXT: br label %[[LATCH]]
+; CHECK: [[LATCH]]:
+; CHECK-NEXT: [[A_NEXT]] = add nsw i32 [[A]], 1
+; CHECK-NEXT: [[B_NEXT]] = add nsw i32 [[B]], 1
+; CHECK-NEXT: [[CMP_OUTER:%.*]] = icmp slt i32 [[A_NEXT]], [[LIMIT]]
+; CHECK-NEXT: br i1 [[CMP_OUTER]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[B_NEXT]]
+;
entry:
br label %loop
@@ -51,16 +60,25 @@ exit:
; @cascade: 'tail' bumps limit and is reachable from two dead sources.
; Neither is provable in isolation; only the fixed point that assumes both
; dead at once can remove them.
-; CHECK-LABEL: define i32 @cascade()
-; CHECK: loop:
-; CHECK-NOT: body1:
-; CHECK-NOT: tail:
-; CHECK-NOT: icmp sgt
-; CHECK-NOT: icmp eq
-; CHECK: mid:
-; CHECK: latch:
-; CHECK: exit:
define i32 @cascade() {
+; CHECK-LABEL: define i32 @cascade() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[A:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A_NEXT:%.*]], %[[LATCH:.*]] ]
+; CHECK-NEXT: [[B:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[LIMIT:%.*]] = phi i32 [ 100, %[[ENTRY]] ], [ [[LIMIT]], %[[LATCH]] ]
+; CHECK-NEXT: br label %[[MID:.*]]
+; CHECK: [[MID]]:
+; CHECK-NEXT: br label %[[LATCH]]
+; CHECK: [[LATCH]]:
+; CHECK-NEXT: [[A_NEXT]] = add nsw i32 [[A]], 1
+; CHECK-NEXT: [[B_NEXT]] = add nsw i32 [[B]], 1
+; CHECK-NEXT: [[CMP_OUT:%.*]] = icmp slt i32 [[A_NEXT]], 100
+; CHECK-NEXT: br i1 [[CMP_OUT]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[LIMIT]]
+;
entry:
br label %loop
diff --git a/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll b/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
index 62a7727bdec18..6b91cf3a180a4 100644
--- a/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
+++ b/llvm/test/Transforms/DeadBranchElimination/phi-merge.ll
@@ -1,15 +1,27 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
; PHI nodes at the merge point must be rewired to the surviving side's value,
; in both fold directions.
-; CHECK-LABEL: define i32 @then_side_dead()
-; CHECK-NOT: then:
-; CHECK: else:
-; CHECK: %b = add nsw i32 %x, 1
-; CHECK-NOT: %x.next = phi
-; CHECK: ret i32 %b
define i32 @then_side_dead() {
+; CHECK-LABEL: define i32 @then_side_dead() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[I:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[I_NEXT:%.*]], %[[MERGE:.*]] ]
+; CHECK-NEXT: [[X:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B:%.*]], %[[MERGE]] ]
+; CHECK-NEXT: br label %[[ELSE:.*]]
+; CHECK: [[ELSE]]:
+; CHECK-NEXT: [[B]] = add nsw i32 [[X]], 1
+; CHECK-NEXT: br label %[[MERGE]]
+; CHECK: [[MERGE]]:
+; CHECK-NEXT: [[I_NEXT]] = add nsw i32 [[I]], 1
+; CHECK-NEXT: [[COND:%.*]] = icmp slt i32 [[I_NEXT]], 10
+; CHECK-NEXT: br i1 [[COND]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[B]]
+;
entry:
br label %loop
@@ -37,13 +49,24 @@ exit:
ret i32 %x.next
}
-; CHECK-LABEL: define i32 @else_side_dead()
-; CHECK: then:
-; CHECK: %a = add nsw i32 %x, 3
-; CHECK-NOT: else:
-; CHECK-NOT: %x.next = phi
-; CHECK: ret i32 %a
define i32 @else_side_dead() {
+; CHECK-LABEL: define i32 @else_side_dead() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[I:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[I_NEXT:%.*]], %[[MERGE:.*]] ]
+; CHECK-NEXT: [[X:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A:%.*]], %[[MERGE]] ]
+; CHECK-NEXT: br label %[[THEN:.*]]
+; CHECK: [[THEN]]:
+; CHECK-NEXT: [[A]] = add nsw i32 [[X]], 3
+; CHECK-NEXT: br label %[[MERGE]]
+; CHECK: [[MERGE]]:
+; CHECK-NEXT: [[I_NEXT]] = add nsw i32 [[I]], 1
+; CHECK-NEXT: [[COND:%.*]] = icmp slt i32 [[I_NEXT]], 10
+; CHECK-NEXT: br i1 [[COND]], label %[[LOOP]], label %[[EXIT:.*]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[A]]
+;
entry:
br label %loop
diff --git a/llvm/test/Transforms/DeadBranchElimination/while-loop.ll b/llvm/test/Transforms/DeadBranchElimination/while-loop.ll
index 1dbb742f588f7..5098e1376f83c 100644
--- a/llvm/test/Transforms/DeadBranchElimination/while-loop.ll
+++ b/llvm/test/Transforms/DeadBranchElimination/while-loop.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=dead-branch-elim -S %s | FileCheck %s
; The motivating example exactly as clang -O0 + mem2reg emits it: an
@@ -6,15 +7,25 @@
; includes the value reached on the exit iteration), i.e. a context-sensitive
; SCEV query.
-; CHECK-LABEL: define i32 @run()
-; CHECK: header:
-; CHECK: body:
-; CHECK-NOT: if.then:
-; CHECK-NOT: icmp eq
-; CHECK: br label %latch
-; CHECK: latch:
-; CHECK: exit:
define i32 @run() {
+; CHECK-LABEL: define i32 @run() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: br label %[[HEADER:.*]]
+; CHECK: [[HEADER]]:
+; CHECK-NEXT: [[LIMIT:%.*]] = phi i32 [ 100, %[[ENTRY]] ], [ [[LIMIT]], %[[LATCH:.*]] ]
+; CHECK-NEXT: [[B:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[A:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[A_NEXT:%.*]], %[[LATCH]] ]
+; CHECK-NEXT: [[GUARD:%.*]] = icmp slt i32 [[A]], [[LIMIT]]
+; CHECK-NEXT: br i1 [[GUARD]], label %[[BODY:.*]], label %[[EXIT:.*]]
+; CHECK: [[BODY]]:
+; CHECK-NEXT: br label %[[LATCH]]
+; CHECK: [[LATCH]]:
+; CHECK-NEXT: [[A_NEXT]] = add nsw i32 [[A]], 1
+; CHECK-NEXT: [[B_NEXT]] = add nsw i32 [[B]], 1
+; CHECK-NEXT: br label %[[HEADER]]
+; CHECK: [[EXIT]]:
+; CHECK-NEXT: ret i32 [[B]]
+;
entry:
br label %header
diff --git a/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
index abc5c89765f49..d49371e79d15c 100644
--- a/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
+++ b/llvm/test/Transforms/PhaseOrdering/dead-branch-elim.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -O2 -S %s | FileCheck %s
; RUN: opt -O2 -enable-dead-branch-elim=false -S %s | FileCheck %s --check-prefix=DISABLED
@@ -16,13 +17,29 @@
; and dead-branch-elim must catch the branch before the GlobalCleanup
; SimplifyCFG turns it into a data dependency.
-; CHECK-LABEL: define {{.*}}i32 @run()
-; CHECK-NEXT: entry:
-; CHECK-NEXT: ret i32 100
-; DISABLED-LABEL: define {{.*}}i32 @run()
-; DISABLED: icmp eq
define i32 @run() {
+; CHECK-LABEL: define noundef i32 @run(
+; CHECK-SAME: ) local_unnamed_addr #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: ret i32 100
+;
+; DISABLED-LABEL: define range(i32 1, 0) i32 @run(
+; DISABLED-SAME: ) local_unnamed_addr #[[ATTR0:[0-9]+]] {
+; DISABLED-NEXT: [[ENTRY:.*]]:
+; DISABLED-NEXT: br label %[[WHILE_BODY:.*]]
+; DISABLED: [[WHILE_BODY]]:
+; DISABLED-NEXT: [[LIMIT_08:%.*]] = phi i32 [ 100, %[[ENTRY]] ], [ [[SPEC_SELECT:%.*]], %[[WHILE_BODY]] ]
+; DISABLED-NEXT: [[B_07:%.*]] = phi i32 [ 0, %[[ENTRY]] ], [ [[B_INC:%.*]], %[[WHILE_BODY]] ]
+; DISABLED-NEXT: [[CMP:%.*]] = icmp eq i32 [[B_07]], [[LIMIT_08]]
+; DISABLED-NEXT: [[INC:%.*]] = zext i1 [[CMP]] to i32
+; DISABLED-NEXT: [[SPEC_SELECT]] = add nuw nsw i32 [[LIMIT_08]], [[INC]]
+; DISABLED-NEXT: [[B_INC]] = add nuw nsw i32 [[B_07]], 1
+; DISABLED-NEXT: [[GUARD:%.*]] = icmp samesign ult i32 [[B_INC]], [[SPEC_SELECT]]
+; DISABLED-NEXT: br i1 [[GUARD]], label %[[WHILE_BODY]], label %[[WHILE_END:.*]]
+; DISABLED: [[WHILE_END]]:
+; DISABLED-NEXT: ret i32 [[B_INC]]
+;
entry:
%a = alloca i32
%b = alloca i32
>From 74ed6d8f7fa75453b32a17157396b0061578df24 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 08:28:56 +0300
Subject: [PATCH 10/13] [DBE] Include ArrayRef.h directly
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index e1d0a74e2bf61..206cb8e8c50df 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -45,6 +45,7 @@
//===----------------------------------------------------------------------===//
#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
+#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DepthFirstIterator.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallPtrSet.h"
>From ea6a5cd763c98bc9290e8891141ac96c70577917 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 08:45:39 +0300
Subject: [PATCH 11/13] [DBE] Materialize assumptions in place instead of
cloning
Replace the CloneFunction-per-iteration detection with an in-place,
fully journaled scheme. The whole effect of assuming a body dead is
expressed at the value level, so the CFG is never modified:
- Virtual reachability: a DFS that skips assumed-dead edges decides
which blocks the assumptions keep alive.
- PHI slot rewriting: in live blocks, slots fed by dead edges or
unreachable regions are overwritten with the surviving value, and
live slots are replaced by their resolved value. Resolution looks
through PHI strongly-connected components (a reference back into the
SCC contributes nothing, generalizing InstSimplify's ignore-self-
references rule; a PHI SCC holds value X iff all inputs entering the
SCC are X, so only top-level results are memoized).
ScalarEvolution then runs on the function in the assumed state via the
private analysis manager; dominating-edge implications from the real
CFG remain valid for assumption-consistent executions, which is what
the fixed-point induction quantifies over. Every write is journaled
and undone in reverse, restoring the exact IR including PHI operand
order: a no-change run leaves the function byte-identical.
This removes all cloning from the pass; a fixed-point iteration now
costs the PHI rewrites plus the ScalarEvolution queries.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Transforms/Scalar/DeadBranchElimination.h | 7 +-
.../Scalar/DeadBranchElimination.cpp | 251 ++++++++++++------
2 files changed, 177 insertions(+), 81 deletions(-)
diff --git a/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h b/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
index 19fdcc06035bf..e634d26c69c52 100644
--- a/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
+++ b/llvm/include/llvm/Transforms/Scalar/DeadBranchElimination.h
@@ -9,9 +9,10 @@
// Eliminates conditional branches that are unreachable, but that cannot be
// proven unreachable directly because the branch body modifies the values the
// condition depends on (a circular dependency). Uses an optimistic fixed
-// point: assume all branch bodies dead, re-run ScalarEvolution on a clone
-// with those bodies removed, and restore every body whose branch edge cannot
-// be proven never-taken, until the assumption set is self-consistent.
+// point: assume all branch bodies dead, temporarily rewrite the PHI slots
+// they feed (journaled and undone in place, no cloning), re-run
+// ScalarEvolution, and restore every body whose branch edge cannot be proven
+// never-taken, until the assumption set is self-consistent.
//
//===----------------------------------------------------------------------===//
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index 206cb8e8c50df..704b5d9d28256 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -24,29 +24,31 @@
// false side) of every conditional branch. Each body starts as Unknown
// ("assumed dead") and can only be promoted to ProvenReachable:
//
-// 1. Clone the function. In the clone, replace every Unknown body with
-// 'unreachable' (redirect the branch edge). Nested branches inside a
-// removed body disappear with it. PHI nodes at merge points drop the
-// incoming values of removed edges, so the bodies' effects vanish.
-// 2. Run ScalarEvolution on the cleaned clone.
-// 3. For each Unknown body, check whether the clone proves its branch edge
-// is never taken (context-sensitive SCEV proof, or the branch sits in
-// unreachable code). If the edge cannot be proven dead, mark it
-// ProvenReachable; its body is restored in the next iteration.
-// 4. Repeat until no status changes. Statuses move in one direction only,
-// so this terminates.
+// 1. Materialize the assumption set in place (no cloning): PHI slots fed
+// by assumed-dead edges or assumed-unreachable regions are temporarily
+// overwritten with the surviving values (see AssumedDeadEdges), so the
+// assumed-dead bodies' effects vanish from the analysis.
+// 2. Run ScalarEvolution on the function in this state.
+// 3. For each Unknown body, check whether the analysis proves its branch
+// edge is never taken (context-sensitive SCEV proof, or the branch
+// sits in an assumed-dead region). If the edge cannot be proven dead,
+// mark it ProvenReachable; its body is restored for the next
+// iteration.
+// 4. Undo the PHI rewrites exactly and repeat until no status changes.
+// Statuses move in one direction only, so this terminates.
//
// At convergence the remaining Unknown set is self-consistent: assuming
// those bodies never run, the analysis proves they indeed never run
-// (consider the first time one would run -- the clone models the program
-// state exactly up to that point and proves the edge is not taken). Those
-// branches are then folded in the original function.
+// (consider the first time one would run -- up to that point the rewritten
+// values match the real execution, and the analysis proves the edge is not
+// taken). Those branches are then folded.
//
//===----------------------------------------------------------------------===//
#include "llvm/Transforms/Scalar/DeadBranchElimination.h"
#include "llvm/ADT/ArrayRef.h"
-#include "llvm/ADT/DepthFirstIterator.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallVector.h"
@@ -64,8 +66,6 @@
#include "llvm/IR/PassInstrumentation.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Debug.h"
-#include "llvm/Transforms/Utils/BasicBlockUtils.h"
-#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/Transforms/Utils/Local.h"
using namespace llvm;
@@ -144,10 +144,11 @@ bool isEdgeProvenDead(ScalarEvolution &SE, CondBrInst *BI, unsigned SuccIdx) {
return false;
}
-/// The analyses are computed on short-lived clone functions, so use a
-/// private, uninstrumented analysis manager rather than the surrounding
-/// pipeline's one: the clones must not show up in pass-manager debug logs,
-/// and their cached results must die with them.
+/// The analyses run on the function while the assumption set is applied, so
+/// use a private, uninstrumented analysis manager rather than the
+/// surrounding pipeline's one: results computed in the assumed state must
+/// never leak into the pipeline's cache, and the extra runs must not show
+/// up in pass-manager debug logs.
FunctionAnalysisManager makePrivateFAM() {
FunctionAnalysisManager FAM;
FAM.registerPass([] { return PassInstrumentationAnalysis(); });
@@ -160,78 +161,172 @@ FunctionAnalysisManager makePrivateFAM() {
return FAM;
}
-/// One fixed-point iteration: rebuild the clone with all Unknown bodies
-/// replaced by 'unreachable', re-run the analysis, and promote every body
-/// whose edge cannot be proven dead. Returns true if any status changed.
-bool refineOnce(Function &F, MutableArrayRef<BranchBody> Bodies) {
- ValueToValueMapTy VMap;
- Function *Clone = CloneFunction(&F, VMap);
- LLVMContext &Ctx = F.getContext();
-
- // Replace every Unknown body with 'unreachable': redirect the branch edge
- // to a trap block. The condition itself is never touched.
- BasicBlock *TrapBB = nullptr;
- for (BranchBody &B : Bodies) {
- if (B.St != Status::Unknown)
- continue;
- auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
- auto *BI = cast<CondBrInst>(BB->getTerminator());
- if (!TrapBB) {
- TrapBB = BasicBlock::Create(Ctx, "dbe.unreachable", Clone);
- new UnreachableInst(Ctx, TrapBB);
+/// Materializes "assume these edges are never taken" directly on the
+/// function, without cloning it. The CFG is never touched -- the whole
+/// effect of a dead body is expressed at the value level:
+///
+/// - Virtual reachability: a DFS from the entry that skips assumed-dead
+/// edges determines which blocks the assumptions keep alive.
+///
+/// - PHI slot rewriting: in every live block, a PHI slot whose edge is
+/// assumed dead (or whose predecessor is virtually unreachable) is
+/// overwritten with a surviving value, and live slots are replaced by
+/// their resolved value (see resolve()). This is what makes the assumed
+/// dead bodies' side effects vanish: the merge PHI of the motivating
+/// example turns into phi [100, ...], [100, ...] and ScalarEvolution
+/// sees the loop limit as the constant it really is.
+///
+/// Every write is journaled and undone in reverse in the destructor, so the
+/// function is restored exactly (including PHI operand order). While the
+/// assumptions are applied the function must only be inspected by analyses,
+/// never verified or transformed.
+class AssumedDeadEdges {
+public:
+ AssumedDeadEdges(Function &F, ArrayRef<BranchBody> Bodies) {
+ for (const BranchBody &B : Bodies)
+ if (B.St == Status::Unknown)
+ DeadEdges.insert({B.BranchBB, B.SuccIdx});
+
+ // Virtual reachability: DFS that does not follow assumed-dead edges.
+ SmallVector<BasicBlock *> Worklist{&F.getEntryBlock()};
+ Reachable.insert(&F.getEntryBlock());
+ while (!Worklist.empty()) {
+ BasicBlock *BB = Worklist.pop_back_val();
+ Instruction *T = BB->getTerminator();
+ for (unsigned I = 0, E = T->getNumSuccessors(); I != E; ++I) {
+ if (DeadEdges.contains({BB, I}))
+ continue;
+ if (Reachable.insert(T->getSuccessor(I)).second)
+ Worklist.push_back(T->getSuccessor(I));
+ }
+ }
+
+ // Rewrite PHI slots in live blocks. Compute all new values first so the
+ // resolver only ever sees original operands.
+ SmallVector<std::tuple<PHINode *, unsigned, Value *>> Rewrites;
+ for (BasicBlock &BB : F) {
+ if (!Reachable.contains(&BB))
+ continue;
+ for (PHINode &PN : BB.phis()) {
+ Value *DeadFill = nullptr;
+ for (unsigned I = 0, E = PN.getNumIncomingValues(); I != E; ++I)
+ if (isLiveSlot(PN, I)) {
+ DeadFill = resolveTopLevel(PN.getIncomingValue(I));
+ break;
+ }
+ assert(DeadFill && "live block with no live PHI slot");
+ for (unsigned I = 0, E = PN.getNumIncomingValues(); I != E; ++I) {
+ Value *NewV = isLiveSlot(PN, I)
+ ? resolveTopLevel(PN.getIncomingValue(I))
+ : DeadFill;
+ if (NewV != PN.getIncomingValue(I))
+ Rewrites.push_back({&PN, I, NewV});
+ }
+ }
+ }
+ for (auto &[PN, Idx, NewV] : Rewrites) {
+ Journal.push_back({PN, Idx, PN->getIncomingValue(Idx)});
+ PN->setIncomingValue(Idx, NewV);
}
- // Drop the PHI entries while the edge still exists, then redirect it.
- BasicBlock *Succ = BI->getSuccessor(B.SuccIdx);
- Succ->removePredecessor(BB);
- BI->setSuccessor(B.SuccIdx, TrapBB);
}
- // A branch nested inside a removed body is itself unreachable in the
- // clone; it stays Unknown and is removed together with its parent.
- SmallPtrSet<BasicBlock *, 32> Reachable;
- for (BasicBlock *BB : depth_first(&Clone->getEntryBlock()))
- Reachable.insert(BB);
- SmallVector<std::pair<BranchBody *, BasicBlock *>> ToCheck;
- for (BranchBody &B : Bodies)
- if (B.St == Status::Unknown) {
- auto *BB = cast<BasicBlock>(VMap[B.BranchBB]);
- if (Reachable.contains(BB))
- ToCheck.push_back({&B, BB});
+ ~AssumedDeadEdges() {
+ for (auto &[PN, Idx, OldV] : reverse(Journal))
+ PN->setIncomingValue(Idx, OldV);
+ }
+
+ bool isReachable(BasicBlock *BB) const { return Reachable.contains(BB); }
+
+private:
+ /// A PHI slot is live when its predecessor is virtually reachable and the
+ /// edge it flows along is not assumed dead.
+ bool isLiveSlot(const PHINode &PN, unsigned Idx) const {
+ BasicBlock *Pred = PN.getIncomingBlock(Idx);
+ if (!Reachable.contains(Pred))
+ return false;
+ Instruction *T = Pred->getTerminator();
+ for (unsigned I = 0, E = T->getNumSuccessors(); I != E; ++I)
+ if (T->getSuccessor(I) == PN.getParent() && DeadEdges.contains({Pred, I}))
+ return false;
+ return true;
+ }
+
+ /// Resolve a value under the assumptions: look through PHI chains whose
+ /// live inputs all agree. A reference back into the PHI strongly-connected
+ /// component under resolution contributes no value (the generalization of
+ /// InstSimplify's "ignore self references" rule): a PHI-SCC holds a single
+ /// value X iff all inputs entering the SCC are X. Only top-level results
+ /// are memoized; intermediate results computed with an incomplete view of
+ /// the SCC would not be valid on their own.
+ Value *resolveTopLevel(Value *V) {
+ Value *R = resolveImpl(V);
+ if (auto *PN = dyn_cast<PHINode>(V))
+ Memo[PN] = R;
+ return R;
+ }
+
+ Value *resolveImpl(Value *V) {
+ auto *PN = dyn_cast<PHINode>(V);
+ if (!PN || !Reachable.contains(PN->getParent()))
+ return V;
+ if (auto It = Memo.find(PN); It != Memo.end())
+ return It->second;
+ if (!Visiting.insert(PN).second)
+ return nullptr; // Cycle back into the SCC: contributes nothing.
+ Value *Common = nullptr;
+ bool Multiple = false;
+ for (unsigned I = 0, E = PN->getNumIncomingValues(); I != E; ++I) {
+ if (!isLiveSlot(*PN, I))
+ continue;
+ Value *R = resolveImpl(PN->getIncomingValue(I));
+ if (!R)
+ continue;
+ if (!Common)
+ Common = R;
+ else if (Common != R) {
+ Multiple = true;
+ break;
+ }
}
+ Visiting.erase(PN);
+ return (Multiple || !Common) ? PN : Common;
+ }
- // Delete the unreachable blocks so PHI nodes in live blocks drop their
- // dead incoming values. DeleteDeadBlocks (unlike removeUnreachableBlocks)
- // never rewrites live terminators, so the branches under test survive.
- SmallVector<BasicBlock *> DeadBlocks;
- for (BasicBlock &BB : *Clone)
- if (!Reachable.contains(&BB))
- DeadBlocks.push_back(&BB);
- DeleteDeadBlocks(DeadBlocks);
+ DenseSet<std::pair<BasicBlock *, unsigned>> DeadEdges;
+ SmallPtrSet<BasicBlock *, 32> Reachable;
+ DenseMap<PHINode *, Value *> Memo;
+ SmallPtrSet<PHINode *, 8> Visiting;
+ SmallVector<std::tuple<PHINode *, unsigned, Value *>> Journal;
+};
+
+/// One fixed-point iteration: apply the current assumption set in place,
+/// re-run the analysis, and promote every body whose edge cannot be proven
+/// dead. Returns true if any status changed.
+bool refineOnce(Function &F, MutableArrayRef<BranchBody> Bodies) {
+ AssumedDeadEdges Assumed(F, Bodies);
bool Changed = false;
{
FunctionAnalysisManager FAM = makePrivateFAM();
- auto &SE = FAM.getResult<ScalarEvolutionAnalysis>(*Clone);
- for (auto &[B, BB] : ToCheck) {
- auto *BI = dyn_cast<CondBrInst>(BB->getTerminator());
- if (!BI) {
- // Something rewrote the branch under test; assume reachable.
- B->St = Status::ProvenReachable;
- Changed = true;
+ auto &SE = FAM.getResult<ScalarEvolutionAnalysis>(F);
+ for (BranchBody &B : Bodies) {
+ // A branch nested inside an assumed-dead region stays Unknown; it is
+ // removed together with its parent.
+ if (B.St != Status::Unknown || !Assumed.isReachable(B.BranchBB))
continue;
- }
- if (!isEdgeProvenDead(SE, BI, B->SuccIdx)) {
- B->St = Status::ProvenReachable;
+ auto *BI = cast<CondBrInst>(B.BranchBB->getTerminator());
+ if (!isEdgeProvenDead(SE, BI, B.SuccIdx)) {
+ B.St = Status::ProvenReachable;
Changed = true;
- LLVM_DEBUG(dbgs() << "DBE: promote " << B->BranchBB->getName() << "/"
- << B->SuccIdx << "\n");
+ LLVM_DEBUG(dbgs() << "DBE: promote " << B.BranchBB->getName() << "/"
+ << B.SuccIdx << "\n");
} else
- LLVM_DEBUG(dbgs() << "DBE: still-dead " << B->BranchBB->getName() << "/"
- << B->SuccIdx << "\n");
+ LLVM_DEBUG(dbgs() << "DBE: still-dead " << B.BranchBB->getName() << "/"
+ << B.SuccIdx << "\n");
}
}
- Clone->eraseFromParent();
+ // The journal in Assumed restores the exact original IR on destruction.
return Changed;
}
>From 8e1a441675089b2a6e96b23b71b9703f5289b3c2 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Wed, 22 Jul 2026 09:35:57 +0300
Subject: [PATCH 12/13] [DBE] Fix stale clone-era comments
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
.../Scalar/DeadBranchElimination.cpp | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
index 704b5d9d28256..73668c918ae12 100644
--- a/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadBranchElimination.cpp
@@ -89,11 +89,11 @@ struct BranchBody {
Status St = Status::Unknown;
};
-/// Cloning and re-running ScalarEvolution is only worthwhile for the
-/// pattern this pass targets: a branch inside a loop whose condition SCEV
-/// can reason about. Everything else is seeded ProvenReachable so that
-/// functions without such branches are never cloned at all. Straight-line
-/// provably-dead branches are left to SCCP/SimplifyCFG.
+/// The fixed point is only worthwhile for the pattern this pass targets: a
+/// branch inside a loop whose condition SCEV can reason about. Everything
+/// else is seeded ProvenReachable so that functions without such branches
+/// are never analyzed at all. Straight-line provably-dead branches are left
+/// to SCCP/SimplifyCFG.
SmallVector<BranchBody> collectBranchBodies(Function &F, LoopInfo &LI) {
SmallVector<BranchBody> Bodies;
for (BasicBlock &BB : F) {
@@ -123,9 +123,9 @@ bool hasCandidateShapedBranch(Function &F) {
return false;
}
-/// Does the clone prove that this branch edge is never taken? The bodies of
-/// all Unknown branches have already been replaced with 'unreachable', so
-/// the condition is evaluated on the cleaned-up code.
+/// Is this branch edge provably never taken? The assumption set is already
+/// applied (see AssumedDeadEdges), so the condition is evaluated as if all
+/// still-Unknown bodies were dead.
bool isEdgeProvenDead(ScalarEvolution &SE, CondBrInst *BI, unsigned SuccIdx) {
Value *Cond = BI->getCondition();
if (auto *CI = dyn_cast<ConstantInt>(Cond))
@@ -399,8 +399,6 @@ bool runOnFunction(Function &F) {
PreservedAnalyses DeadBranchEliminationPass::run(Module &M,
ModuleAnalysisManager &AM) {
bool Changed = false;
- // This is a module pass (it temporarily creates function clones), but it
- // transforms functions independently.
for (Function &F : M) {
if (F.isDeclaration() || F.isPresplitCoroutine() || F.hasOptNone())
continue;
>From 121b91afdfaa45156d6d984bd1477a7da0ec8ed7 Mon Sep 17 00:00:00 2001
From: Raz Luvaton <16746759+rluvaton at users.noreply.github.com>
Date: Thu, 23 Jul 2026 20:47:24 +0300
Subject: [PATCH 13/13] [DBE] Run the pass again after the inliner
The existing placement in module simplification runs before the
inliner, so a circular-dependency branch whose body lives in a callee
is invisible to it: the caller only has an opaque call, and inside the
callee nothing is provable about the arguments. The canonical example
is a reserve-then-push-n-times loop, where the len == cap grow branch
only materializes as a provably dead branch once push is inlined into
the counted loop.
Add a second run at the start of the module optimization pipeline
(post-inline, ahead of the loop passes so the vectorizer sees the
cleaned loops). Such branches still exist at this point because their
bodies contain calls, which SimplifyCFG cannot speculate into selects.
The two placements cover disjoint patterns, and the new
pipeline-position test pins each one: the arithmetic-only pattern must
fold before GlobalCleanupPM's SimplifyCFG speculates it into selects
(before-inline placement), and the cross-function pattern only exists
after inlining (after-inline placement). Removing either placement
fails the corresponding function's checks.
Co-Authored-By: Claude Fable 5 <noreply at anthropic.com>
---
llvm/lib/Passes/PassBuilderPipelines.cpp | 9 ++
llvm/test/Other/new-pm-defaults.ll | 1 +
.../Other/new-pm-thinlto-postlink-defaults.ll | 1 +
.../new-pm-thinlto-postlink-pgo-defaults.ll | 1 +
...-pm-thinlto-postlink-samplepgo-defaults.ll | 1 +
.../pipeline-position.ll | 129 ++++++++++++++++++
6 files changed, 142 insertions(+)
create mode 100644 llvm/test/Transforms/DeadBranchElimination/pipeline-position.ll
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index 26c31086e39c5..858d1c23ce7af 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -1529,6 +1529,15 @@ PassBuilder::buildModuleOptimizationPipeline(OptimizationLevel Level,
ThinOrFullLTOPhase LTOPhase) {
ModulePassManager MPM;
+ // A second dead-branch-elimination run, now that inlining has exposed
+ // checks whose circular dependencies span function boundaries (e.g. a
+ // container's grow-on-full check after its accessors were inlined). Such
+ // branches survive to this point because their bodies contain calls, which
+ // SimplifyCFG cannot speculate into selects. Running before the loop
+ // passes lets the vectorizer see the cleaned loops.
+ if (EnableDeadBranchElimination)
+ MPM.addPass(DeadBranchEliminationPass());
+
// Run partial inlining pass to partially inline functions that have
// large bodies.
if (RunPartialInlining)
diff --git a/llvm/test/Other/new-pm-defaults.ll b/llvm/test/Other/new-pm-defaults.ll
index 378f6db4d2a74..ffcf8cc32f6d9 100644
--- a/llvm/test/Other/new-pm-defaults.ll
+++ b/llvm/test/Other/new-pm-defaults.ll
@@ -226,6 +226,7 @@
; CHECK-O-NEXT: Running pass: CoroCleanupPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
; CHECK-O-NEXT: Running pass: GlobalDCEPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-DEFAULT-NEXT: Running pass: EliminateAvailableExternallyPass
; CHECK-LTO-NOT: Running pass: EliminateAvailableExternallyPass
; CHECK-O-NEXT: Running pass: ReversePostOrderFunctionAttrsPass
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
index 6d981e1428542..90a100b11b05a 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-defaults.ll
@@ -158,6 +158,7 @@
; CHECK-O-NEXT: Running pass: CoroCleanupPass
; CHECK-POSTLINK-O-NEXT: Running pass: GlobalOptPass
; CHECK-POSTLINK-O-NEXT: Running pass: GlobalDCEPass
+; CHECK-POSTLINK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-POSTLINK-O-NEXT: Running pass: EliminateAvailableExternallyPass
; CHECK-POSTLINK-O-NEXT: Running pass: ReversePostOrderFunctionAttrsPass
; CHECK-POSTLINK-O-NEXT: Running pass: RecomputeGlobalsAAPass
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
index 052c441d74bad..7b2c41208a159 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll
@@ -142,6 +142,7 @@
; CHECK-O-NEXT: Running pass: CoroCleanupPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
; CHECK-O-NEXT: Running pass: GlobalDCEPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: EliminateAvailableExternallyPass
; CHECK-O-NEXT: Running pass: ReversePostOrderFunctionAttrsPass
; CHECK-O-NEXT: Running pass: RecomputeGlobalsAAPass
diff --git a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
index bc0447a4640d8..51d019e4fdb55 100644
--- a/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
+++ b/llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll
@@ -149,6 +149,7 @@
; CHECK-O-NEXT: Running pass: CoroCleanupPass
; CHECK-O-NEXT: Running pass: GlobalOptPass
; CHECK-O-NEXT: Running pass: GlobalDCEPass
+; CHECK-O-NEXT: Running pass: DeadBranchEliminationPass
; CHECK-O-NEXT: Running pass: EliminateAvailableExternallyPass
; CHECK-O-NEXT: Running pass: ReversePostOrderFunctionAttrsPass
; CHECK-O-NEXT: Running pass: RecomputeGlobalsAAPass
diff --git a/llvm/test/Transforms/DeadBranchElimination/pipeline-position.ll b/llvm/test/Transforms/DeadBranchElimination/pipeline-position.ll
new file mode 100644
index 0000000000000..22f16835195e7
--- /dev/null
+++ b/llvm/test/Transforms/DeadBranchElimination/pipeline-position.ll
@@ -0,0 +1,129 @@
+; Verify BOTH pipeline placements of DeadBranchElimination are required.
+; Each function below is only optimizable at one of the two placements, so
+; removing either placement makes one of the positive checks fail:
+;
+; @circular_limit needs the BEFORE-INLINE placement (in module
+; simplification, ahead of GlobalCleanupPM's SimplifyCFG). Its dead body
+; `limit += 1` is side-effect free, so a later SimplifyCFG speculates it
+; into a select (`limit += zext(b == limit)`), destroying the branch
+; before a post-inline pass instance could ever see it.
+;
+; @fill needs the AFTER-INLINE placement (start of module optimization).
+; Before inlining, the dead `len == cap` branch lives in @push where
+; nothing is provable about its arguments, and @fill contains no
+; candidate branch at all; only after @push is inlined does the
+; circular-dependency pattern exist in a single function. Its dead body
+; contains a call, so SimplifyCFG can never speculate it away and it
+; survives to the later placement.
+;
+; RUN: opt -passes='default<O2>' -S %s | FileCheck %s
+; RUN: opt -passes='default<O2>' -enable-dead-branch-elim=false -S %s \
+; RUN: | FileCheck %s --check-prefix=DISABLED
+
+; int circular_limit() {
+; int a = 0, b = 0, limit = 100;
+; while (a < limit) {
+; if (b == limit) // unreachable, but modifies limit
+; limit += 1;
+; a++; b++;
+; }
+; return b;
+; }
+; Raw (un-optimized) clang output shape: locals in allocas so the branch
+; reaches the before-inline placement exactly as it would from clang.
+
+; CHECK-LABEL: @circular_limit(
+; CHECK-NOT: br
+; CHECK: ret i32 100
+;
+; DISABLED-LABEL: @circular_limit(
+; DISABLED: br i1
+
+define i32 @circular_limit() nounwind {
+ %a = alloca i32, align 4
+ %b = alloca i32, align 4
+ %limit = alloca i32, align 4
+ store i32 0, ptr %a, align 4
+ store i32 0, ptr %b, align 4
+ store i32 100, ptr %limit, align 4
+ br label %header
+
+header:
+ %a.cur = load i32, ptr %a, align 4
+ %limit.cur = load i32, ptr %limit, align 4
+ %in.bounds = icmp slt i32 %a.cur, %limit.cur
+ br i1 %in.bounds, label %body, label %exit
+
+body:
+ %b.cur = load i32, ptr %b, align 4
+ %limit.cur2 = load i32, ptr %limit, align 4
+ %hit.limit = icmp eq i32 %b.cur, %limit.cur2
+ br i1 %hit.limit, label %bump, label %latch
+
+bump:
+ %limit.cur3 = load i32, ptr %limit, align 4
+ %limit.next = add nsw i32 %limit.cur3, 1
+ store i32 %limit.next, ptr %limit, align 4
+ br label %latch
+
+latch:
+ %a.cur2 = load i32, ptr %a, align 4
+ %a.next = add nsw i32 %a.cur2, 1
+ store i32 %a.next, ptr %a, align 4
+ %b.cur2 = load i32, ptr %b, align 4
+ %b.next = add nsw i32 %b.cur2, 1
+ store i32 %b.next, ptr %b, align 4
+ br label %header, !llvm.loop !0
+
+exit:
+ %b.final = load i32, ptr %b, align 4
+ ret i32 %b.final
+}
+
+; Vec-push shape: @fill(n) reserves capacity n, then pushes n times; the
+; grow path inside @push is dead but only provably so after inlining.
+
+; CHECK-LABEL: @fill(
+; CHECK-NOT: @grow
+; CHECK: ret i64 %n
+;
+; DISABLED-LABEL: @fill(
+; DISABLED: call {{.*}}@grow
+
+declare i64 @grow(i64)
+
+define internal i64 @push(i64 %len, i64 %cap) nounwind {
+entry:
+ %full = icmp eq i64 %len, %cap
+ br i1 %full, label %do.grow, label %done
+
+do.grow:
+ %cap.grown = call i64 @grow(i64 %cap)
+ br label %done
+
+done:
+ %cap.out = phi i64 [ %cap, %entry ], [ %cap.grown, %do.grow ]
+ ret i64 %cap.out
+}
+
+define i64 @fill(i64 %n) nounwind {
+entry:
+ br label %header
+
+header:
+ %i = phi i64 [ 0, %entry ], [ %i.next, %latch ]
+ %cap = phi i64 [ %n, %entry ], [ %cap.next, %latch ]
+ %more = icmp ult i64 %i, %n
+ br i1 %more, label %latch, label %exit
+
+latch:
+ %cap.next = call i64 @push(i64 %i, i64 %cap)
+ %i.next = add nuw i64 %i, 1
+ br label %header, !llvm.loop !0
+
+exit:
+ ret i64 %cap
+}
+
+!0 = distinct !{!0, !1}
+!1 = !{!"llvm.loop.mustprogress"}
More information about the llvm-commits
mailing list