[llvm] SimplifyCFG: fold trivial non-empty blocks with safe instructions (PR #180350)
Ridham Khurana via llvm-commits
llvm-commits at lists.llvm.org
Sat Feb 7 07:40:26 PST 2026
https://github.com/rmkhurana28 updated https://github.com/llvm/llvm-project/pull/180350
>From fc23e21110e15509ce9143028e7bc7aa5c3128b2 Mon Sep 17 00:00:00 2001
From: Ridham Khurana <khurana.ridham222 at gmail.com>
Date: Sat, 7 Feb 2026 19:43:33 +0530
Subject: [PATCH 1/2] SimplifyCFG: fold trivial non-empty blocks with safe
instructions
---
llvm/lib/Transforms/Utils/SimplifyCFG.cpp | 217 ++++++++++++++++++
.../trivial-nonempty-fold-final.ll | 74 ++++++
2 files changed, 291 insertions(+)
create mode 100644 llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index 8b56109990b21..a33bd33790def 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -8367,6 +8367,217 @@ static bool tryToMergeLandingPad(LandingPadInst *LPad, BranchInst *BI,
return false;
}
+/// Return true if \/p I is safe to keep in a “trivial block” that we intend to
+/// merge away.
+///
+/// This predicate is intentionally conservative and only whitelists a small set
+/// of obviously-safe instructions. Anything not explicitly allowed returns
+/// false.
+static bool isSafeInstructionForTrivialMerge(const Instruction *I) {
+ assert(I && "Expected non-null instruction");
+
+ // Control-flow and SSA plumbing are handled elsewhere.
+ if (I->isTerminator() || isa<PHINode>(I))
+ return false;
+
+ // Debug and lifetime markers are safe to ignore.
+ if (I->isDebugOrPseudoInst() || I->isLifetimeStartOrEnd())
+ return true;
+
+ // Explicitly reject fences / fence-like operations.
+ if (I->getOpcode() == Instruction::Fence || I->isFenceLike())
+ return false;
+
+ // Stack allocations should not be moved/merged by this transform.
+ if (isa<AllocaInst>(I))
+ return false;
+
+ // Reject anything that may unwind.
+ if (I->mayThrow())
+ return false;
+
+ // Handle memory operations explicitly.
+ if (const auto *LI = dyn_cast<LoadInst>(I))
+ return LI->isSimple();
+ if (const auto *SI = dyn_cast<StoreInst>(I))
+ return SI->isSimple();
+
+ // Calls/intrinsics: only allow “pure” (readnone) and nounwind, non-convergent.
+ if (const auto *CB = dyn_cast<CallBase>(I)) {
+ if (CB->isConvergent())
+ return false;
+ return CB->doesNotAccessMemory() && CB->doesNotThrow();
+ }
+
+ // Anything with side effects is unsafe.
+ if (I->mayHaveSideEffects())
+ return false;
+
+ // Whitelist a small set of trivially-safe, side-effect-free opcodes.
+ switch (I->getOpcode()) {
+ // Integer/FP arithmetic and logical ops.
+ case Instruction::Add:
+ case Instruction::Sub:
+ case Instruction::Mul:
+ case Instruction::UDiv:
+ case Instruction::SDiv:
+ case Instruction::URem:
+ case Instruction::SRem:
+ case Instruction::FAdd:
+ case Instruction::FSub:
+ case Instruction::FMul:
+ case Instruction::FDiv:
+ case Instruction::FRem:
+ case Instruction::And:
+ case Instruction::Or:
+ case Instruction::Xor:
+ case Instruction::Shl:
+ case Instruction::LShr:
+ case Instruction::AShr:
+
+ // Comparisons.
+ case Instruction::ICmp:
+ case Instruction::FCmp:
+
+ // Casts / pointer-int conversions.
+ case Instruction::Trunc:
+ case Instruction::ZExt:
+ case Instruction::SExt:
+ case Instruction::FPToUI:
+ case Instruction::FPToSI:
+ case Instruction::UIToFP:
+ case Instruction::SIToFP:
+ case Instruction::FPTrunc:
+ case Instruction::FPExt:
+ case Instruction::PtrToInt:
+ case Instruction::IntToPtr:
+ case Instruction::BitCast:
+ case Instruction::AddrSpaceCast:
+
+ // Address calculation.
+ case Instruction::GetElementPtr:
+
+ // Select and vector/aggregate element ops.
+ case Instruction::Select:
+ case Instruction::ExtractElement:
+ case Instruction::InsertElement:
+ case Instruction::ShuffleVector:
+ case Instruction::ExtractValue:
+ case Instruction::InsertValue:
+ return true;
+
+ default:
+ break;
+ }
+
+ // Unknown/unhandled instruction: be conservative.
+ return false;
+}
+
+/// Return true if \/p BB is a “trivial but non-empty” block that is safe for
+/// SimplifyCFG to fold away (subject to additional CFG/PHI legality checks).
+///
+/// This is a purely syntactic and conservative predicate:
+/// - Requires an unconditional `br` terminator.
+/// - Skips PHIs and debug/lifetime markers.
+/// - Requires all remaining non-terminator instructions to be explicitly
+/// whitelisted by isSafeInstructionForTrivialMerge().
+static bool isTrivialNonEmptyBlockSafeToFold(const BasicBlock *BB) {
+ assert(BB && "Expected non-null basic block");
+
+ const auto *BI = dyn_cast<BranchInst>(BB->getTerminator());
+ if (!BI || !BI->isUnconditional())
+ return false;
+
+ for (const Instruction &Inst : *BB) {
+ const Instruction *I = &Inst;
+ if (isa<PHINode>(I))
+ continue;
+ if (I->isDebugOrPseudoInst() || I->isLifetimeStartOrEnd())
+ continue;
+ if (I == BI)
+ continue;
+ if (!isSafeInstructionForTrivialMerge(I))
+ return false;
+ }
+
+ return true;
+}
+
+/// Single-predecessor version of non-empty trivial block folding.
+///
+/// This is a minimal, conservative extension in the spirit of
+/// TryToSimplifyUncondBranchFromEmptyBlock(): it only splices instructions into
+/// the unique predecessor (no cloning/duplication) and performs a simple PHI
+/// fixup in the successor.
+static bool TryToSimplifyUncondBranchFromNonEmptyBlock(BasicBlock *BB,
+ DomTreeUpdater *DTU) {
+ assert(BB && "Expected non-null BB");
+
+ if (BB == &BB->getParent()->getEntryBlock())
+ return false;
+
+ if (!isTrivialNonEmptyBlockSafeToFold(BB))
+ return false;
+
+ auto *BBBr = cast<BranchInst>(BB->getTerminator());
+ BasicBlock *Succ = BBBr->getSuccessor(0);
+ if (Succ == BB)
+ return false;
+
+ // Only handle the single-predecessor case.
+ BasicBlock *Pred = BB->getSinglePredecessor();
+ if (!Pred)
+ return false;
+
+ // Be conservative: avoid folding blocks with PHIs for now.
+ if (!BB->phis().empty())
+ return false;
+
+ auto *PredBr = dyn_cast<BranchInst>(Pred->getTerminator());
+ if (!PredBr || !PredBr->isUnconditional() || PredBr->getSuccessor(0) != BB)
+ return false;
+
+ // Update PHIs in the successor: replace incoming block BB with Pred.
+ for (PHINode &PN : Succ->phis()) {
+ int BBIdx = PN.getBasicBlockIndex(BB);
+ if (BBIdx < 0)
+ continue;
+ if (count(PN.blocks(), BB) != 1)
+ return false;
+ PN.setIncomingBlock(BBIdx, Pred);
+ }
+
+ // Splice all whitelisted instructions (excluding debug/lifetime and the
+ // terminator) into Pred, immediately before Pred's terminator.
+ Instruction *InsertBefore = Pred->getTerminator();
+ for (auto It = BB->getFirstNonPHIOrDbg(), End = BB->end(); It != End;) {
+ Instruction *I = &*It++;
+ if (I == BBBr)
+ break;
+ if (I->isDebugOrPseudoInst() || I->isLifetimeStartOrEnd())
+ continue;
+ assert(isSafeInstructionForTrivialMerge(I) &&
+ "Unexpected instruction in supposedly-trivial block");
+ I->moveBefore(InsertBefore);
+ }
+
+ // Redirect Pred's branch to Succ.
+ PredBr->setSuccessor(0, Succ);
+
+ if (DTU) {
+ DTU->applyUpdates({
+ {DominatorTree::Insert, Pred, Succ},
+ {DominatorTree::Delete, Pred, BB},
+ {DominatorTree::Delete, BB, Succ},
+ });
+ }
+
+ BB->dropAllReferences();
+ BB->eraseFromParent();
+ return true;
+}
+
bool SimplifyCFGOpt::simplifyBranch(BranchInst *Branch, IRBuilder<> &Builder) {
return Branch->isUnconditional() ? simplifyUncondBranch(Branch, Builder)
: simplifyCondBranch(Branch, Builder);
@@ -8393,6 +8604,12 @@ bool SimplifyCFGOpt::simplifyUncondBranch(BranchInst *BI,
!NeedCanonicalLoop && TryToSimplifyUncondBranchFromEmptyBlock(BB, DTU))
return true;
+ // If the block is not empty, try a conservative single-predecessor fold for
+ // trivial blocks that contain only whitelisted instructions.
+ if (!I->isTerminator() && BB != &BB->getParent()->getEntryBlock() &&
+ !NeedCanonicalLoop && TryToSimplifyUncondBranchFromNonEmptyBlock(BB, DTU))
+ return true;
+
// If the only instruction in the block is a seteq/setne comparison against a
// constant, try to simplify the block.
if (ICmpInst *ICI = dyn_cast<ICmpInst>(I)) {
diff --git a/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll b/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
new file mode 100644
index 0000000000000..5f0251fed8d4b
--- /dev/null
+++ b/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
@@ -0,0 +1,74 @@
+; RUN: opt -S -passes=simplifycfg < %s | FileCheck %s
+; RUN: opt -S -passes='simplifycfg' < %s | FileCheck %s
+
+; CHECK-LABEL: @fold_load
+; CHECK-NOT: bb1
+; CHECK: load i32
+define i32 @fold_load(i1 %cond, ptr %p) {
+entry:
+ br i1 %cond, label %pred, label %other
+
+pred:
+ %d = add i32 1, 1
+ br label %bb1
+
+other:
+ %d2 = add i32 2, 2
+ br label %exit
+
+bb1:
+ %x = load i32, ptr %p
+ br label %exit
+
+exit:
+ %phi = phi i32 [ %x, %bb1 ], [ 42, %other ]
+ ret i32 %phi
+}
+
+; CHECK-LABEL: @fold_arith
+; CHECK-NOT: bb1
+; CHECK: add i32 %a, 1
+define i32 @fold_arith(i32 %a, i1 %cond) {
+entry:
+ br i1 %cond, label %pred, label %other
+
+pred:
+ %d = add i32 3, 3
+ br label %bb1
+
+other:
+ %d2 = add i32 4, 4
+ br label %exit
+
+bb1:
+ %x = add i32 %a, 1
+ br label %exit
+
+exit:
+ %phi = phi i32 [ %x, %bb1 ], [ %a, %other ]
+ ret i32 %phi
+}
+
+; CHECK-LABEL: @no_fold_multi_store
+; CHECK: bb1:
+; CHECK: store i32
+; CHECK: ret void
+define void @no_fold_multi_store(ptr %p, i1 %cond) {
+entry:
+ br i1 %cond, label %A, label %B
+
+A:
+ %dA = add i32 10, 1
+ br label %bb1
+
+B:
+ %dB = add i32 20, 1
+ br label %bb1
+
+bb1:
+ store i32 10, ptr %p
+ br label %exit
+
+exit:
+ ret void
+}
>From 6f204ada296f5a7f2453ced3fcf390c8f4371396 Mon Sep 17 00:00:00 2001
From: Ridham Khurana <khurana.ridham222 at gmail.com>
Date: Sat, 7 Feb 2026 20:52:10 +0530
Subject: [PATCH 2/2] simplifycfg: preserve debug/lifetime intrinsics, refine
comments, add store fold test
---
llvm/lib/Transforms/Utils/SimplifyCFG.cpp | 9 ++++-----
.../SimplifyCFG/trivial-nonempty-fold-final.ll | 13 +++++++++++++
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index a33bd33790def..0a269610717c5 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -8409,7 +8409,8 @@ static bool isSafeInstructionForTrivialMerge(const Instruction *I) {
return CB->doesNotAccessMemory() && CB->doesNotThrow();
}
- // Anything with side effects is unsafe.
+ // Remaining side-effecting instructions (beyond simple loads/stores and pure,
+ // non-throwing calls handled above) are unsafe to fold.
if (I->mayHaveSideEffects())
return false;
@@ -8548,15 +8549,13 @@ static bool TryToSimplifyUncondBranchFromNonEmptyBlock(BasicBlock *BB,
PN.setIncomingBlock(BBIdx, Pred);
}
- // Splice all whitelisted instructions (excluding debug/lifetime and the
- // terminator) into Pred, immediately before Pred's terminator.
+ // Splice all whitelisted instructions (including debug/lifetime markers,
+ // excluding the terminator) into Pred, immediately before Pred's terminator.
Instruction *InsertBefore = Pred->getTerminator();
for (auto It = BB->getFirstNonPHIOrDbg(), End = BB->end(); It != End;) {
Instruction *I = &*It++;
if (I == BBBr)
break;
- if (I->isDebugOrPseudoInst() || I->isLifetimeStartOrEnd())
- continue;
assert(isSafeInstructionForTrivialMerge(I) &&
"Unexpected instruction in supposedly-trivial block");
I->moveBefore(InsertBefore);
diff --git a/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll b/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
index 5f0251fed8d4b..07b29f945f897 100644
--- a/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
+++ b/llvm/test/Transforms/SimplifyCFG/trivial-nonempty-fold-final.ll
@@ -72,3 +72,16 @@ bb1:
exit:
ret void
}
+
+; CHECK-LABEL: @fold_store
+; CHECK: store i32
+; CHECK: ret void
+define void @fold_store(ptr %p) {
+entry:
+ br label %mid
+mid:
+ store i32 5, ptr %p
+ br label %exit
+exit:
+ ret void
+}
More information about the llvm-commits
mailing list