[llvm] [JumpThreading] Thread a common icmp across PHI nodes (PR #221125)
Abhay Kanhere via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 8 10:54:45 PDT 2026
https://github.com/AbhayKanhere updated https://github.com/llvm/llvm-project/pull/221125
>From 5aa655b070a1f4ced005ea502151e325c84f4f6e Mon Sep 17 00:00:00 2001
From: Abhay Kanhere <a_kanhere at apple.com>
Date: Thu, 3 Sep 2026 10:32:40 -0700
Subject: [PATCH 1/2] [JumpThreading] Thread a common icmp across PHI nodes
When both icmp operands are block PHIs, use isImpliedCondition of the
predecessor's branch to recover its per-edge value and thread known edges.
---
llvm/lib/Transforms/Scalar/JumpThreading.cpp | 28 ++++
.../JumpThreading/thread-cmp-of-phis.ll | 134 ++++++++++++++++++
2 files changed, 162 insertions(+)
create mode 100644 llvm/test/Transforms/JumpThreading/thread-cmp-of-phis.ll
diff --git a/llvm/lib/Transforms/Scalar/JumpThreading.cpp b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
index 7a0542aac83fc..203165bb27672 100644
--- a/llvm/lib/Transforms/Scalar/JumpThreading.cpp
+++ b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
@@ -767,6 +767,34 @@ bool JumpThreadingPass::computeValueKnownInPredecessorsImpl(
RHS = PN->getIncomingValue(i);
}
Value *Res = simplifyCmpInst(Pred, LHS, RHS, {DL});
+
+ // Even without folding, the compare may be known on this edge if
+ // PredBB's branch tests a condition that implies it -- most usefully
+ // the same compare, when both operands are PHIs. PredBB's false arm
+ // reaches BB, so C is false there; BB re-tests C via the PHIs:
+ //
+ // PredBB: br (C = icmp <pred> A, B), T, BB ; BB is the false arm
+ // \
+ // v
+ // BB: L = phi [A, PredBB], ... ; L -> A on the PredBB edge
+ // R = phi [B, PredBB], ... ; R -> B on the PredBB edge
+ // br (icmp <pred> L, R) ; == C, false => thread past it
+ //
+ // Require both operands available on the edge (not defined in BB).
+ if (!Res && isa<ICmpInst>(Cmp) && !CmpType->isVectorTy()) {
+ auto DefinedInBB = [&](Value *V) {
+ auto *VI = dyn_cast<Instruction>(V);
+ return VI && VI->getParent() == BB;
+ };
+ auto *PredBI = dyn_cast<CondBrInst>(PredBB->getTerminator());
+ if (PredBI && !DefinedInBB(LHS) && !DefinedInBB(RHS) &&
+ PredBI->getSuccessor(0) != PredBI->getSuccessor(1))
+ if (std::optional<bool> Implied = isImpliedCondition(
+ PredBI->getCondition(), Pred, LHS, RHS, DL,
+ /*LHSIsTrue=*/PredBI->getSuccessor(0) == BB))
+ Res = ConstantInt::getBool(Cmp->getContext(), *Implied);
+ }
+
if (!Res) {
if (!isa<Constant>(RHS))
continue;
diff --git a/llvm/test/Transforms/JumpThreading/thread-cmp-of-phis.ll b/llvm/test/Transforms/JumpThreading/thread-cmp-of-phis.ll
new file mode 100644
index 0000000000000..d56ae2ae6a4e9
--- /dev/null
+++ b/llvm/test/Transforms/JumpThreading/thread-cmp-of-phis.ll
@@ -0,0 +1,134 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt -passes=jump-threading -S < %s | FileCheck %s
+;
+; JumpThreading can thread a block whose condition is `icmp <pred> (phiA, phiB)`
+; (both operands PHIs defined in the block) on the predecessor edges where the
+; predecessor's controlling branch implies the comparison. Threading is partial:
+; only edges with a proven result are threaded; others keep the branch.
+
+declare i64 @opaque()
+
+; Positive: on the %entry edge the PHIs reconstruct to `icmp eq %a, %b` = %c
+; (false there), so thread %entry -> %if.ne; the opaque %grow edge keeps its branch.
+define i32 @thread_cmp_of_phis(i64 %a, i64 %b) {
+; CHECK-LABEL: @thread_cmp_of_phis(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[C:%.*]] = icmp eq i64 [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: br i1 [[C]], label [[JOIN:%.*]], label [[IF_NE:%.*]]
+; CHECK: join:
+; CHECK-NEXT: [[A2:%.*]] = call i64 @opaque()
+; CHECK-NEXT: [[B2:%.*]] = call i64 @opaque()
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq i64 [[A2]], [[B2]]
+; CHECK-NEXT: br i1 [[CMP]], label [[IF_EQ:%.*]], label [[IF_NE]]
+; CHECK: if.eq:
+; CHECK-NEXT: ret i32 1
+; CHECK: if.ne:
+; CHECK-NEXT: ret i32 0
+;
+entry:
+ %c = icmp eq i64 %a, %b
+ br i1 %c, label %grow, label %join
+
+grow:
+ %a2 = call i64 @opaque()
+ %b2 = call i64 @opaque()
+ br label %join
+
+join:
+ %p1 = phi i64 [ %a, %entry ], [ %a2, %grow ]
+ %p2 = phi i64 [ %b, %entry ], [ %b2, %grow ]
+ %cmp = icmp eq i64 %p1, %p2
+ br i1 %cmp, label %if.eq, label %if.ne
+
+if.eq:
+ ret i32 1
+
+if.ne:
+ ret i32 0
+}
+
+; Negative: %c compares %a/%b, but the join re-tests %a2/%b2 -- different
+; operands, so %c does not imply it; %entry is not threaded.
+define i32 @no_thread_operands_differ(i64 %a, i64 %b, i64 %a2, i64 %b2) {
+; CHECK-LABEL: @no_thread_operands_differ(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[C:%.*]] = icmp eq i64 [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: br i1 [[C]], label [[MID:%.*]], label [[JOIN:%.*]]
+; CHECK: mid:
+; CHECK-NEXT: [[PA:%.*]] = call i64 @opaque()
+; CHECK-NEXT: [[PB:%.*]] = call i64 @opaque()
+; CHECK-NEXT: br label [[JOIN]]
+; CHECK: join:
+; CHECK-NEXT: [[P1:%.*]] = phi i64 [ [[A2:%.*]], [[ENTRY:%.*]] ], [ [[PA]], [[MID]] ]
+; CHECK-NEXT: [[P2:%.*]] = phi i64 [ [[B2:%.*]], [[ENTRY]] ], [ [[PB]], [[MID]] ]
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq i64 [[P1]], [[P2]]
+; CHECK-NEXT: br i1 [[CMP]], label [[IF_EQ:%.*]], label [[IF_NE:%.*]]
+; CHECK: if.eq:
+; CHECK-NEXT: ret i32 1
+; CHECK: if.ne:
+; CHECK-NEXT: ret i32 0
+;
+entry:
+ %c = icmp eq i64 %a, %b
+ br i1 %c, label %mid, label %join
+
+mid:
+ %pa = call i64 @opaque()
+ %pb = call i64 @opaque()
+ br label %join
+
+join:
+ %p1 = phi i64 [ %a2, %entry ], [ %pa, %mid ]
+ %p2 = phi i64 [ %b2, %entry ], [ %pb, %mid ]
+ %cmp = icmp eq i64 %p1, %p2
+ br i1 %cmp, label %if.eq, label %if.ne
+
+if.eq:
+ ret i32 1
+
+if.ne:
+ ret i32 0
+}
+
+; Negative: same operands, but %c is unsigned (ult) while the join re-tests
+; signed (slt) -- ult does not imply slt; %entry is not threaded.
+define i32 @no_thread_predicate_differs(i64 %a, i64 %b) {
+; CHECK-LABEL: @no_thread_predicate_differs(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[C:%.*]] = icmp ult i64 [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: br i1 [[C]], label [[GROW:%.*]], label [[JOIN:%.*]]
+; CHECK: grow:
+; CHECK-NEXT: [[A2:%.*]] = call i64 @opaque()
+; CHECK-NEXT: [[B2:%.*]] = call i64 @opaque()
+; CHECK-NEXT: br label [[JOIN]]
+; CHECK: join:
+; CHECK-NEXT: [[P1:%.*]] = phi i64 [ [[A]], [[ENTRY:%.*]] ], [ [[A2]], [[GROW]] ]
+; CHECK-NEXT: [[P2:%.*]] = phi i64 [ [[B]], [[ENTRY]] ], [ [[B2]], [[GROW]] ]
+; CHECK-NEXT: [[CMP:%.*]] = icmp slt i64 [[P1]], [[P2]]
+; CHECK-NEXT: br i1 [[CMP]], label [[IF_EQ:%.*]], label [[IF_NE:%.*]]
+; CHECK: if.eq:
+; CHECK-NEXT: ret i32 1
+; CHECK: if.ne:
+; CHECK-NEXT: ret i32 0
+;
+entry:
+ %c = icmp ult i64 %a, %b
+ br i1 %c, label %grow, label %join
+
+grow:
+ %a2 = call i64 @opaque()
+ %b2 = call i64 @opaque()
+ br label %join
+
+join:
+ %p1 = phi i64 [ %a, %entry ], [ %a2, %grow ]
+ %p2 = phi i64 [ %b, %entry ], [ %b2, %grow ]
+ %cmp = icmp slt i64 %p1, %p2
+ br i1 %cmp, label %if.eq, label %if.ne
+
+if.eq:
+ ret i32 1
+
+if.ne:
+ ret i32 0
+}
>From 0454212f84f7995936977a3e5a2a96c319dd3ccb Mon Sep 17 00:00:00 2001
From: Abhay Kanhere <a_kanhere at apple.com>
Date: Tue, 8 Sep 2026 10:40:07 -0700
Subject: [PATCH 2/2] [JumpThreading] Address review feedback; refactor for
readability (NFC)
Extract the edge-implied-compare check into a static isImpliedByEdgeBranch
helper (keeping the CFG diagram). No functional change.
---
llvm/lib/Transforms/Scalar/JumpThreading.cpp | 62 ++++++++++++--------
1 file changed, 36 insertions(+), 26 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/JumpThreading.cpp b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
index 203165bb27672..3e20249478306 100644
--- a/llvm/lib/Transforms/Scalar/JumpThreading.cpp
+++ b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
@@ -549,6 +549,36 @@ static Constant *getKnownConstant(Value *Val, ConstantPreference Preference) {
return dyn_cast<ConstantInt>(Val);
}
+/// If the edge PredBB->BB is controlled by a conditional branch whose condition
+/// implies `icmp Pred LHS, RHS`, return that compare's value on the edge (else
+/// std::nullopt). LHS/RHS are the operands as seen on the edge and must be
+/// available there -- a compare operand defined in BB does not exist on the
+/// edge. Most useful when both compare operands are PHIs in BB and PredBB
+/// already branched on the same compare:
+///
+/// PredBB: br (C = icmp <pred> A, B), T, BB ; here BB is the false arm
+/// \
+/// v
+/// BB: L = phi [A, PredBB], ... ; L -> A on the PredBB edge
+/// R = phi [B, PredBB], ... ; R -> B on the PredBB edge
+/// br (icmp <pred> L, R) ; == C, false => thread past it
+static std::optional<bool> isImpliedByEdgeBranch(BasicBlock *PredBB,
+ BasicBlock *BB,
+ CmpInst::Predicate Pred,
+ Value *LHS, Value *RHS,
+ const DataLayout &DL) {
+ auto DefinedInBB = [&](Value *V) {
+ auto *I = dyn_cast<Instruction>(V);
+ return I && I->getParent() == BB;
+ };
+ auto *PredBI = dyn_cast<CondBrInst>(PredBB->getTerminator());
+ if (!PredBI || PredBI->getSuccessor(0) == PredBI->getSuccessor(1) ||
+ DefinedInBB(LHS) || DefinedInBB(RHS))
+ return std::nullopt;
+ return isImpliedCondition(PredBI->getCondition(), Pred, LHS, RHS, DL,
+ /*LHSIsTrue=*/PredBI->getSuccessor(0) == BB);
+}
+
/// computeValueKnownInPredecessors - Given a basic block BB and a value V, see
/// if we can infer that the value is a known ConstantInt/BlockAddress or undef
/// in any of our predecessors. If so, return the known list of value and pred
@@ -768,32 +798,12 @@ bool JumpThreadingPass::computeValueKnownInPredecessorsImpl(
}
Value *Res = simplifyCmpInst(Pred, LHS, RHS, {DL});
- // Even without folding, the compare may be known on this edge if
- // PredBB's branch tests a condition that implies it -- most usefully
- // the same compare, when both operands are PHIs. PredBB's false arm
- // reaches BB, so C is false there; BB re-tests C via the PHIs:
- //
- // PredBB: br (C = icmp <pred> A, B), T, BB ; BB is the false arm
- // \
- // v
- // BB: L = phi [A, PredBB], ... ; L -> A on the PredBB edge
- // R = phi [B, PredBB], ... ; R -> B on the PredBB edge
- // br (icmp <pred> L, R) ; == C, false => thread past it
- //
- // Require both operands available on the edge (not defined in BB).
- if (!Res && isa<ICmpInst>(Cmp) && !CmpType->isVectorTy()) {
- auto DefinedInBB = [&](Value *V) {
- auto *VI = dyn_cast<Instruction>(V);
- return VI && VI->getParent() == BB;
- };
- auto *PredBI = dyn_cast<CondBrInst>(PredBB->getTerminator());
- if (PredBI && !DefinedInBB(LHS) && !DefinedInBB(RHS) &&
- PredBI->getSuccessor(0) != PredBI->getSuccessor(1))
- if (std::optional<bool> Implied = isImpliedCondition(
- PredBI->getCondition(), Pred, LHS, RHS, DL,
- /*LHSIsTrue=*/PredBI->getSuccessor(0) == BB))
- Res = ConstantInt::getBool(Cmp->getContext(), *Implied);
- }
+ // If it doesn't fold, the compare may still be known on this edge when
+ // PredBB's branch condition implies it (see isImpliedByEdgeBranch).
+ if (!Res && isa<ICmpInst>(Cmp) && !CmpType->isVectorTy())
+ if (std::optional<bool> Implied =
+ isImpliedByEdgeBranch(PredBB, BB, Pred, LHS, RHS, DL))
+ Res = ConstantInt::getBool(Cmp->getContext(), *Implied);
if (!Res) {
if (!isa<Constant>(RHS))
More information about the llvm-commits
mailing list