[llvm] [ValueTracking] Support multiple predecessors in willNotFreeBetween (PR #223580)
Vy Nguyen via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 17 06:06:17 PDT 2026
https://github.com/oontvoo updated https://github.com/llvm/llvm-project/pull/223580
>From bdb114f0ad42bf2d373242a3961b8735dbded96a Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Mon, 14 Sep 2026 22:14:37 -0400
Subject: [PATCH 1/4] [ValueTracking] Support multiple predecessors in
willNotFreeBetween
Previously, `willNotFreeBetween` only walked backward along a linear chain of single-predecessor blocks, bailing out whenever control flow branched and merged (eg., conditional `if-else` blocks before a loop preheader).
This PR generalises the backward walk to use a worklist over all predecessors from `CtxBB` to `AssumeBB` (bounded by `MaxInstrsToCheckForFree`).
This enables LICM( and possibly other passes) to prove dereferenceability and hoist invariant loads across merging control flow when no path contains a freeing instruction.
---
llvm/lib/Analysis/ValueTracking.cpp | 38 +++++++++++--------
llvm/unittests/Analysis/ValueTrackingTest.cpp | 34 +++++++++++++++++
2 files changed, 57 insertions(+), 15 deletions(-)
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index ac49dbc8d76d3..472582f9b383d 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -761,22 +761,30 @@ bool llvm::willNotFreeBetween(const Instruction *Assume,
return hasNoFreeInRange(make_range(Assume->getIterator(), CtxIter));
}
- // Handle chain of single-predecessor blocks.
- const BasicBlock *CurBB = CtxBB;
- while (true) {
- if (CurBB == AssumeBB)
- return hasNoFreeInRange(
- make_range(Assume->getIterator(), AssumeBB->end()));
-
- const BasicBlock *PredBB = CurBB->getSinglePredecessor();
- if (!PredBB)
- return false;
-
- if (!hasNoFreeInRange(make_range(CurBB->begin(),
- CurBB == CtxBB ? CtxIter : CurBB->end())))
- return false;
- CurBB = PredBB;
+ // Multi-predecessor worklist.
+ SmallVector<const BasicBlock *, 8> Worklist;
+ SmallPtrSet<const BasicBlock *, 8> Visited;
+ Worklist.push_back(CtxBB);
+ Visited.insert(CtxBB);
+ while (!Worklist.empty()) {
+ const BasicBlock *CurBB = Worklist.pop_back_val();
+ if (CurBB == AssumeBB) {
+ if (!hasNoFreeInRange(
+ make_range(Assume->getIterator(), AssumeBB->end())))
+ return false;
+ continue;
+ }
+ if (!hasNoFreeInRange(make_range(
+ CurBB->begin(), CurBB == CtxBB ? CtxIter : CurBB->end())))
+ return false;
+ if (pred_empty(CurBB))
+ return false;
+ for (const BasicBlock *Pred : predecessors(CurBB)) {
+ if (Visited.insert(Pred).second)
+ Worklist.push_back(Pred);
+ }
}
+ return true;
}
// TODO: cmpExcludesZero misses many cases where `RHS` is non-constant but
diff --git a/llvm/unittests/Analysis/ValueTrackingTest.cpp b/llvm/unittests/Analysis/ValueTrackingTest.cpp
index 70731e34185de..4aa9beca62fe0 100644
--- a/llvm/unittests/Analysis/ValueTrackingTest.cpp
+++ b/llvm/unittests/Analysis/ValueTrackingTest.cpp
@@ -3412,6 +3412,40 @@ TEST_F(ValueTrackingTest, HaveNoCommonBitsSet) {
}
}
+TEST_F(ValueTrackingTest, willNotFreeBetweenMultiPredecessor) {
+ StringRef Assembly = R"(
+ declare void @llvm.assume(i1)
+ declare void @may_free()
+ declare void @no_free() nofree
+ define void @test(i1 %cond, ptr %p) {
+ entry:
+ %A = call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %p, i64 16) ]
+ br i1 %cond, label %if.then, label %if.else
+ if.then:
+ call void @no_free()
+ br label %merge
+ if.else:
+ br label %merge
+ merge:
+ %CxtI = load i32, ptr %p
+ br i1 %cond, label %if.then2, label %if.else2
+ if.then2:
+ call void @may_free()
+ br label %merge2
+ if.else2:
+ br label %merge2
+ merge2:
+ %CxtI2 = load i32, ptr %p
+ ret void
+ }
+ )";
+ parseAssembly(Assembly);
+ // Both paths from %A to %CxtI only call nofree functions -> should return true.
+ EXPECT_TRUE(willNotFreeBetween(A, CxtI));
+ // One path from %A to %CxtI2 calls @may_free() -> should return false.
+ EXPECT_FALSE(willNotFreeBetween(A, CxtI2));
+}
+
class IsBytewiseValueTest : public ValueTrackingTest,
public ::testing::WithParamInterface<
std::pair<const char *, const char *>> {
>From 5a20056044c6af328951ae74aae73cd22e3add21 Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Mon, 14 Sep 2026 22:35:01 -0400
Subject: [PATCH 2/4] formatting
---
llvm/lib/Analysis/ValueTracking.cpp | 29 +++++++++----------
llvm/unittests/Analysis/ValueTrackingTest.cpp | 3 +-
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index 472582f9b383d..ae7a29a34cd1e 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -767,22 +767,21 @@ bool llvm::willNotFreeBetween(const Instruction *Assume,
Worklist.push_back(CtxBB);
Visited.insert(CtxBB);
while (!Worklist.empty()) {
- const BasicBlock *CurBB = Worklist.pop_back_val();
- if (CurBB == AssumeBB) {
- if (!hasNoFreeInRange(
- make_range(Assume->getIterator(), AssumeBB->end())))
- return false;
- continue;
- }
- if (!hasNoFreeInRange(make_range(
- CurBB->begin(), CurBB == CtxBB ? CtxIter : CurBB->end())))
+ const BasicBlock *CurBB = Worklist.pop_back_val();
+ if (CurBB == AssumeBB) {
+ if (!hasNoFreeInRange(make_range(Assume->getIterator(), AssumeBB->end())))
return false;
- if (pred_empty(CurBB))
- return false;
- for (const BasicBlock *Pred : predecessors(CurBB)) {
- if (Visited.insert(Pred).second)
- Worklist.push_back(Pred);
- }
+ continue;
+ }
+ if (!hasNoFreeInRange(make_range(CurBB->begin(),
+ CurBB == CtxBB ? CtxIter : CurBB->end())))
+ return false;
+ if (pred_empty(CurBB))
+ return false;
+ for (const BasicBlock *Pred : predecessors(CurBB)) {
+ if (Visited.insert(Pred).second)
+ Worklist.push_back(Pred);
+ }
}
return true;
}
diff --git a/llvm/unittests/Analysis/ValueTrackingTest.cpp b/llvm/unittests/Analysis/ValueTrackingTest.cpp
index 4aa9beca62fe0..35135d12b2226 100644
--- a/llvm/unittests/Analysis/ValueTrackingTest.cpp
+++ b/llvm/unittests/Analysis/ValueTrackingTest.cpp
@@ -3440,7 +3440,8 @@ TEST_F(ValueTrackingTest, willNotFreeBetweenMultiPredecessor) {
}
)";
parseAssembly(Assembly);
- // Both paths from %A to %CxtI only call nofree functions -> should return true.
+ // Both paths from %A to %CxtI only call nofree functions -> should return
+ // true.
EXPECT_TRUE(willNotFreeBetween(A, CxtI));
// One path from %A to %CxtI2 calls @may_free() -> should return false.
EXPECT_FALSE(willNotFreeBetween(A, CxtI2));
>From 641281794fd766b386a4f25b1927e6b8ad84a673 Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Tue, 15 Sep 2026 17:51:46 -0400
Subject: [PATCH 3/4] address review comments: - handle loop back edges -
replace unit test with lit test - update other tests
---
llvm/lib/Analysis/ValueTracking.cpp | 54 ++++++++++---------
.../ValueTracking/memory-dereferenceable.ll | 43 +++++++++++++++
.../LICM/hoist-speculatable-load.ll | 2 +-
...able-info-from-assumption-constant-size.ll | 6 +--
llvm/unittests/Analysis/ValueTrackingTest.cpp | 35 ------------
5 files changed, 77 insertions(+), 63 deletions(-)
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index ae7a29a34cd1e..2d4ac35d8bc90 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -733,49 +733,55 @@ bool llvm::isValidAssumeForContext(const Instruction *Inv,
return false;
}
+static bool hasNoFreeInRange(BasicBlock::const_iterator Begin,
+ BasicBlock::const_iterator End,
+ unsigned &NumChecked) {
+ for (const Instruction &I : make_range(Begin, End)) {
+ if (NumChecked++ > MaxInstrsToCheckForFree)
+ return false;
+ if (auto *CB = dyn_cast<CallBase>(&I)) {
+ if (!CB->hasFnAttr(Attribute::NoFree))
+ return false;
+ } else if (I.maySynchronize())
+ return false;
+ }
+ return true;
+}
+
bool llvm::willNotFreeBetween(const Instruction *Assume,
const Instruction *CtxI) {
- // Helper to check if there are any calls in the range that may free memory.
unsigned NumChecked = 0;
- auto hasNoFreeInRange = [&NumChecked](auto Range) {
- for (const Instruction &I : Range) {
- if (NumChecked++ > MaxInstrsToCheckForFree)
- return false;
-
- if (auto *CB = dyn_cast<CallBase>(&I)) {
- if (!CB->hasFnAttr(Attribute::NoFree))
- return false;
- } else if (I.maySynchronize())
- return false;
- }
- return true;
- };
-
const BasicBlock *CtxBB = CtxI->getParent();
const BasicBlock *AssumeBB = Assume->getParent();
BasicBlock::const_iterator CtxIter = CtxI->getIterator();
if (CtxBB == AssumeBB) {
- // Same block case: check that Assume comes before CtxI.
if (Assume != CtxI && !Assume->comesBefore(CtxI))
return false;
- return hasNoFreeInRange(make_range(Assume->getIterator(), CtxIter));
+ return hasNoFreeInRange(Assume->getIterator(), CtxIter, NumChecked);
}
-
- // Multi-predecessor worklist.
+ // Check instructions before CtxI in CtxBB.
+ if (!hasNoFreeInRange(CtxBB->begin(), CtxIter, NumChecked))
+ return false;
+ if (pred_empty(CtxBB))
+ return false;
SmallVector<const BasicBlock *, 8> Worklist;
SmallPtrSet<const BasicBlock *, 8> Visited;
- Worklist.push_back(CtxBB);
- Visited.insert(CtxBB);
+ for (const BasicBlock *Pred : predecessors(CtxBB)) {
+ if (Visited.insert(Pred).second)
+ Worklist.push_back(Pred);
+ }
while (!Worklist.empty()) {
const BasicBlock *CurBB = Worklist.pop_back_val();
if (CurBB == AssumeBB) {
- if (!hasNoFreeInRange(make_range(Assume->getIterator(), AssumeBB->end())))
+ if (!hasNoFreeInRange(Assume->getIterator(), AssumeBB->end(), NumChecked))
return false;
continue;
}
- if (!hasNoFreeInRange(make_range(CurBB->begin(),
- CurBB == CtxBB ? CtxIter : CurBB->end())))
+ if (!hasNoFreeInRange(CurBB == CtxBB ? CtxIter : CurBB->begin(),
+ CurBB->end(), NumChecked))
return false;
+ if (CurBB == CtxBB)
+ continue;
if (pred_empty(CurBB))
return false;
for (const BasicBlock *Pred : predecessors(CurBB)) {
diff --git a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
index 9fbf27c3d7e72..299e9d5e4496f 100644
--- a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
+++ b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
@@ -411,6 +411,49 @@ declare ptr addrspace(1) @func1(ptr addrspace(1) returned) nounwind argmemonly
; Can free any object accessible in memory
declare void @mayfree()
+; CHECK-LABEL: 'dereferenceable_arg_multi_pred_nofree'
+; CHECK: %a
+define void @dereferenceable_arg_multi_pred_nofree(ptr dereferenceable(16) %a, i1 %cond) {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ call void @mayfree() nofree
+ br label %merge
+if.else:
+ br label %merge
+merge:
+ %v = load i32, ptr %a
+ ret void
+}
+; CHECK-LABEL: 'dereferenceable_arg_multi_pred_freed'
+; GLOBAL: %a
+; POINT-NOT: %a
+define void @dereferenceable_arg_multi_pred_freed(ptr dereferenceable(16) %a, i1 %cond) {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ call void @mayfree()
+ br label %merge
+if.else:
+ br label %merge
+merge:
+ %v = load i32, ptr %a
+ ret void
+}
+; CHECK-LABEL: 'dereferenceable_arg_loop_backedge_freed'
+; GLOBAL: %a
+; POINT-NOT: %a
+define void @dereferenceable_arg_loop_backedge_freed(ptr dereferenceable(16) %a, i1 %again) {
+entry:
+ br label %loop
+loop:
+ %v = load i32, ptr %a
+ call void @mayfree()
+ br i1 %again, label %loop, label %exit
+exit:
+ ret void
+}
+
!0 = !{i64 4}
!1 = !{i64 2}
!2 = !{}
diff --git a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
index ab754a1f9a9d6..05c3982e080e6 100644
--- a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
+++ b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
@@ -66,12 +66,12 @@ define void @f_nofree_nosync(i32 %ptr_i, ptr %ptr2, i1 %cond) nofree nosync {
; CHECK-NEXT: store i32 0, ptr [[PTR2:%.*]], align 4
; CHECK-NEXT: br label [[FOR_BODY_LR_PH]]
; CHECK: for.body.lr.ph:
+; CHECK-NEXT: [[TMP0:%.*]] = load i32, ptr [[PTR]], align 4
; CHECK-NEXT: br label [[FOR_BODY:%.*]]
; CHECK: for.body:
; CHECK-NEXT: [[I_08:%.*]] = phi i32 [ 0, [[FOR_BODY_LR_PH]] ], [ [[INC:%.*]], [[IF_END:%.*]] ]
; CHECK-NEXT: br i1 [[COND]], label [[IF_END]], label [[IF:%.*]]
; CHECK: if:
-; CHECK-NEXT: [[TMP0:%.*]] = load i32, ptr [[PTR]], align 4, !invariant.load [[META0]]
; CHECK-NEXT: store i32 [[TMP0]], ptr [[PTR2]], align 4
; CHECK-NEXT: br label [[IF_END]]
; CHECK: if.end:
diff --git a/llvm/test/Transforms/LoopVectorize/dereferenceable-info-from-assumption-constant-size.ll b/llvm/test/Transforms/LoopVectorize/dereferenceable-info-from-assumption-constant-size.ll
index 21a2dd94542b8..20e49af5e4b7e 100644
--- a/llvm/test/Transforms/LoopVectorize/dereferenceable-info-from-assumption-constant-size.ll
+++ b/llvm/test/Transforms/LoopVectorize/dereferenceable-info-from-assumption-constant-size.ll
@@ -1182,9 +1182,9 @@ define void @deref_assumption_in_header_constant_trip_count_multiple_loop_predec
; CHECK-NEXT: [[TMP0:%.*]] = getelementptr i32, ptr [[A]], i64 [[INDEX]]
; CHECK-NEXT: [[TMP1:%.*]] = getelementptr inbounds i32, ptr [[B]], i64 [[INDEX]]
; CHECK-NEXT: [[WIDE_LOAD:%.*]] = load <2 x i32>, ptr [[TMP1]], align 4
-; CHECK-NEXT: [[TMP2:%.*]] = icmp slt <2 x i32> [[WIDE_LOAD]], zeroinitializer
-; CHECK-NEXT: [[WIDE_MASKED_LOAD:%.*]] = call <2 x i32> @llvm.masked.load.v2i32.p0(ptr align 4 [[TMP0]], <2 x i1> [[TMP2]], <2 x i32> poison)
-; CHECK-NEXT: [[PREDPHI:%.*]] = select <2 x i1> [[TMP2]], <2 x i32> [[WIDE_MASKED_LOAD]], <2 x i32> [[WIDE_LOAD]]
+; CHECK-NEXT: [[TMP2:%.*]] = icmp sge <2 x i32> [[WIDE_LOAD]], zeroinitializer
+; CHECK-NEXT: [[WIDE_LOAD1:%.*]] = load <2 x i32>, ptr [[TMP0]], align 4
+; CHECK-NEXT: [[PREDPHI:%.*]] = select <2 x i1> [[TMP2]], <2 x i32> [[WIDE_LOAD]], <2 x i32> [[WIDE_LOAD1]]
; CHECK-NEXT: [[TMP3:%.*]] = getelementptr inbounds i32, ptr [[C]], i64 [[INDEX]]
; CHECK-NEXT: store <2 x i32> [[PREDPHI]], ptr [[TMP3]], align 4
; CHECK-NEXT: [[INDEX_NEXT]] = add nuw i64 [[INDEX]], 2
diff --git a/llvm/unittests/Analysis/ValueTrackingTest.cpp b/llvm/unittests/Analysis/ValueTrackingTest.cpp
index 35135d12b2226..70731e34185de 100644
--- a/llvm/unittests/Analysis/ValueTrackingTest.cpp
+++ b/llvm/unittests/Analysis/ValueTrackingTest.cpp
@@ -3412,41 +3412,6 @@ TEST_F(ValueTrackingTest, HaveNoCommonBitsSet) {
}
}
-TEST_F(ValueTrackingTest, willNotFreeBetweenMultiPredecessor) {
- StringRef Assembly = R"(
- declare void @llvm.assume(i1)
- declare void @may_free()
- declare void @no_free() nofree
- define void @test(i1 %cond, ptr %p) {
- entry:
- %A = call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %p, i64 16) ]
- br i1 %cond, label %if.then, label %if.else
- if.then:
- call void @no_free()
- br label %merge
- if.else:
- br label %merge
- merge:
- %CxtI = load i32, ptr %p
- br i1 %cond, label %if.then2, label %if.else2
- if.then2:
- call void @may_free()
- br label %merge2
- if.else2:
- br label %merge2
- merge2:
- %CxtI2 = load i32, ptr %p
- ret void
- }
- )";
- parseAssembly(Assembly);
- // Both paths from %A to %CxtI only call nofree functions -> should return
- // true.
- EXPECT_TRUE(willNotFreeBetween(A, CxtI));
- // One path from %A to %CxtI2 calls @may_free() -> should return false.
- EXPECT_FALSE(willNotFreeBetween(A, CxtI2));
-}
-
class IsBytewiseValueTest : public ValueTrackingTest,
public ::testing::WithParamInterface<
std::pair<const char *, const char *>> {
>From 4638c094595722c7d4eec41ace094fd919789575 Mon Sep 17 00:00:00 2001
From: Vy Nguyen <vyng at google.com>
Date: Thu, 17 Sep 2026 09:05:59 -0400
Subject: [PATCH 4/4] formatting
---
llvm/lib/Analysis/ValueTracking.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index 2d4ac35d8bc90..bbeb73946d7cb 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -742,8 +742,9 @@ static bool hasNoFreeInRange(BasicBlock::const_iterator Begin,
if (auto *CB = dyn_cast<CallBase>(&I)) {
if (!CB->hasFnAttr(Attribute::NoFree))
return false;
- } else if (I.maySynchronize())
+ } else if (I.maySynchronize()) {
return false;
+ }
}
return true;
}
More information about the llvm-commits
mailing list