[llvm] [TailCallElim] Add flow-sensitivity to alloca escape analysis (PR #176825)
Federico Bruzzone via llvm-commits
llvm-commits at lists.llvm.org
Tue Jan 20 12:53:04 PST 2026
https://github.com/FedericoBruzzone updated https://github.com/llvm/llvm-project/pull/176825
>From 0e4eb79d84679c61636e3d786ef6447badabfab9 Mon Sep 17 00:00:00 2001
From: FedericoBruzzone <federico.bruzzone.i at gmail.com>
Date: Mon, 19 Jan 2026 22:15:05 +0100
Subject: [PATCH] [TailCallElim] Add flow-sensitivity to alloca escape analysis
The current Tail Call Elimination (TRE) pass is overly conservative when
an alloca's address is stored into another memory location. Any store
automatically marks the alloca as "escaped," preventing TRE even if the
address never actually leaves the function's scope.
This patch enhances AllocaDerivedValueTracker by:
1. Utilizing DominatorTree to provide flow-sensitivity.
2. Transitively tracking addresses stored into other local allocas.
3. Only considering "forward" uses (dominated by the store) as potential
escapes.
This allows TRE to be applied in cases where allocas are used as temporary
storage (e.g., pointers to local variables stored in other slots) if
those addresses do not reach the recursive call site.
Signed-off-by: FedericoBruzzone <federico.bruzzone.i at gmail.com>
---
.../Scalar/TailRecursionElimination.h | 2 +-
.../Scalar/TailRecursionElimination.cpp | 92 ++++++++++++++-----
.../alloca-forward-dominance-reuse.ll | 45 +++++++++
.../TailCallElim/alloca-temporal-alias.ll | 41 +++++++++
.../TailCallElim/prevent-tre-alloca-escape.ll | 45 +++++++++
.../TailCallElim/tre-stats-improvement.ll | 63 +++++++++++++
6 files changed, 265 insertions(+), 23 deletions(-)
create mode 100644 llvm/test/Transforms/TailCallElim/alloca-forward-dominance-reuse.ll
create mode 100644 llvm/test/Transforms/TailCallElim/alloca-temporal-alias.ll
create mode 100644 llvm/test/Transforms/TailCallElim/prevent-tre-alloca-escape.ll
create mode 100644 llvm/test/Transforms/TailCallElim/tre-stats-improvement.ll
diff --git a/llvm/include/llvm/Transforms/Scalar/TailRecursionElimination.h b/llvm/include/llvm/Transforms/Scalar/TailRecursionElimination.h
index 22a70cd66865a..4c2bcff01f34c 100644
--- a/llvm/include/llvm/Transforms/Scalar/TailRecursionElimination.h
+++ b/llvm/include/llvm/Transforms/Scalar/TailRecursionElimination.h
@@ -66,6 +66,6 @@ class TailCallElimPass : public PassInfoMixin<TailCallElimPass> {
: UpdateFunctionEntryCount(UpdateFunctionEntryCount) {}
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};
-}
+} // namespace llvm
#endif // LLVM_TRANSFORMS_SCALAR_TAILRECURSIONELIMINATION_H
diff --git a/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp b/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
index 89d41f3e40de7..8b583681dc560 100644
--- a/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
@@ -87,7 +87,7 @@ using namespace llvm;
#define DEBUG_TYPE "tailcallelim"
STATISTIC(NumEliminated, "Number of tail calls removed");
-STATISTIC(NumRetDuped, "Number of return duplicated");
+STATISTIC(NumRetDuped, "Number of return duplicated");
STATISTIC(NumAccumAdded, "Number of accumulators introduced");
static cl::opt<bool> ForceDisableBFI(
@@ -110,6 +110,8 @@ static bool canTRE(Function &F) {
namespace {
struct AllocaDerivedValueTracker {
+ explicit AllocaDerivedValueTracker(DominatorTree &DT) : DT(DT) {}
+
// Start at a root value and walk its use-def chain to mark calls that use the
// value or a derived value in AllocaUsers, and places where it may escape in
// EscapePoints.
@@ -117,10 +119,25 @@ struct AllocaDerivedValueTracker {
SmallVector<Use *, 32> Worklist;
SmallPtrSet<Use *, 32> Visited;
- auto AddUsesToWorklist = [&](Value *V) {
- for (auto &U : V->uses()) {
+ // Populate the worklist with the uses of the given value.
+ // 'StartInst' when provided allows for path-sensitive tracking via
+ // DominatorTree. That is, we only consider uses that are dominated by
+ // 'StartInst'.
+ auto AddUsesToWorklist = [&](Value *Root,
+ std::optional<Instruction *> StartInst =
+ std::nullopt) {
+ for (auto &U : Root->uses()) {
+ Instruction *UserInst = cast<Instruction>(U.getUser());
+
+ if (StartInst.has_value()) {
+ Instruction *SI = StartInst.value();
+ if (!DT.dominates(SI, UserInst))
+ continue;
+ }
+
if (!Visited.insert(&U).second)
continue;
+
Worklist.push_back(&U);
}
};
@@ -152,14 +169,46 @@ struct AllocaDerivedValueTracker {
break;
}
case Instruction::Load: {
- // The result of a load is not alloca-derived (unless an alloca has
- // otherwise escaped, but this is a local analysis).
- continue;
+ // If we load a pointer from a tracked location, that pointer might
+ // be the address of a local alloca (if we stored it there earlier).
+ // We need to track where that loaded pointer goes.
+ // However, if the loaded value is not a pointer, it cannot be an
+ // alloca address, so we don't need to track it.
+ if (!I->getType()->isPointerTy())
+ continue;
+ break;
}
case Instruction::Store: {
- if (U->getOperandNo() == 0)
- EscapePoints.insert(I);
- continue; // Stores have no users to analyze.
+ // If the pointer we are tracking is being stored as a Value
+ // (Operand 0), its address is being leaked into another memory
+ // location.
+ if (U->getOperandNo() == 0) {
+ auto *SI = cast<StoreInst>(I);
+ // If the destination is an alloca, we can propagate the
+ // tracking to it.
+ //
+ // We only collect "forward" uses (those dominated by this
+ // store).
+ // Rationale: Any access to 'Dest' that happens
+ // chronologically before this instruction cannot possibly
+ // contain the address of our tracked pointer.
+ //
+ // By filtering for forward uses, we provide a degree of
+ // flow-sensitivity that avoids false-positive escapes from
+ // previous, unrelated life-cycles of the memory.
+ if (Value *Dest = SI->getPointerOperand()->stripPointerCasts();
+ isa<AllocaInst>(Dest)) {
+ AddUsesToWorklist(Dest, SI);
+ } else {
+ // If the destination is a Global, a Heap allocation, or a
+ // pointer passed as an argument, we assume the address has
+ // escaped the function's control.
+ EscapePoints.insert(I);
+ }
+ }
+ // Stores have no users to analyze because they don't produce a
+ // value.
+ continue;
}
case Instruction::BitCast:
case Instruction::GetElementPtr:
@@ -191,15 +240,17 @@ struct AllocaDerivedValueTracker {
SmallPtrSet<Instruction *, 32> AllocaUsers;
SmallPtrSet<Instruction *, 32> EscapePoints;
+ DominatorTree &DT;
};
} // namespace
-static bool markTails(Function &F, OptimizationRemarkEmitter *ORE) {
+static bool markTails(Function &F, OptimizationRemarkEmitter *ORE,
+ DominatorTree &DT) {
if (F.callsFunctionThatReturnsTwice())
return false;
// The local stack holds all alloca instructions and all byval arguments.
- AllocaDerivedValueTracker Tracker;
+ AllocaDerivedValueTracker Tracker(DT);
for (Argument &Arg : F.args()) {
if (Arg.hasByValAttr())
Tracker.walk(&Arg);
@@ -215,11 +266,7 @@ static bool markTails(Function &F, OptimizationRemarkEmitter *ORE) {
// Track whether a block is reachable after an alloca has escaped. Blocks that
// contain the escaping instruction will be marked as being visited without an
// escaped alloca, since that is how the block began.
- enum VisitType {
- UNVISITED,
- UNESCAPED,
- ESCAPED
- };
+ enum VisitType { UNVISITED, UNESCAPED, ESCAPED };
DenseMap<BasicBlock *, VisitType> Visited;
// We propagate the fact that an alloca has escaped from block to successor.
@@ -348,7 +395,7 @@ static bool canMoveAboveCall(Instruction *I, CallInst *CI, AliasAnalysis *AA) {
// FIXME: We can move load/store/call/free instructions above the call if the
// call does not mod/ref the memory location being processed.
- if (I->mayHaveSideEffects()) // This also handles volatile loads.
+ if (I->mayHaveSideEffects()) // This also handles volatile loads.
return false;
if (LoadInst *L = dyn_cast<LoadInst>(I)) {
@@ -491,7 +538,7 @@ CallInst *TailRecursionEliminator::findTRECandidate(BasicBlock *BB) {
break;
if (BBI == BB->begin())
- return nullptr; // Didn't find a potential tail call.
+ return nullptr; // Didn't find a potential tail call.
--BBI;
}
@@ -512,7 +559,8 @@ CallInst *TailRecursionEliminator::findTRECandidate(BasicBlock *BB) {
auto I = CI->arg_begin(), E = CI->arg_end();
Function::arg_iterator FI = F.arg_begin(), FE = F.arg_end();
for (; I != E && FI != FE; ++I, ++FI)
- if (*I != &*FI) break;
+ if (*I != &*FI)
+ break;
if (I == E && FI == FE)
return nullptr;
}
@@ -752,8 +800,8 @@ bool TailRecursionEliminator::eliminateCall(CallInst *CI) {
BranchInst *NewBI = BranchInst::Create(HeaderBB, Ret->getIterator());
NewBI->setDebugLoc(CI->getDebugLoc());
- Ret->eraseFromParent(); // Remove return.
- CI->eraseFromParent(); // Remove call.
+ Ret->eraseFromParent(); // Remove return.
+ CI->eraseFromParent(); // Remove call.
DTU.applyUpdates({{DominatorTree::Insert, BB, HeaderBB}});
++NumEliminated;
if (OrigEntryBBFreq) {
@@ -910,7 +958,7 @@ bool TailRecursionEliminator::eliminate(Function &F,
return false;
bool MadeChange = false;
- MadeChange |= markTails(F, ORE);
+ MadeChange |= markTails(F, ORE, DTU.getDomTree());
// If this function is a varargs function, we won't be able to PHI the args
// right, so don't even try to convert it...
diff --git a/llvm/test/Transforms/TailCallElim/alloca-forward-dominance-reuse.ll b/llvm/test/Transforms/TailCallElim/alloca-forward-dominance-reuse.ll
new file mode 100644
index 0000000000000..08b300bd06607
--- /dev/null
+++ b/llvm/test/Transforms/TailCallElim/alloca-forward-dominance-reuse.ll
@@ -0,0 +1,45 @@
+; RUN: opt -passes=tailcallelim -S < %s | FileCheck %s
+
+; Test that Tail Call Elimination (TRE) can occur even when an alloca's address
+; is stored in memory, provided that the DominatorTree-based analysis can prove
+; that the storage is local and the address does not escape into the recursive call.
+
+define i32 @test_alloca_reuse(i32 %n, i32 %acc) {
+; CHECK-LABEL: define i32 @test_alloca_reuse(i32 %n, i32 %acc)
+; CHECK: entry:
+; CHECK: %temp_slot = alloca ptr
+; CHECK: br label %tailrecurse
+
+; CHECK: tailrecurse:
+; CHECK-NEXT: %n.tr = phi i32 [ %n, %entry ], [ %n_dec, %recurse ]
+; CHECK-NEXT: %acc.tr = phi i32 [ %acc, %entry ], [ %acc_inc, %recurse ]
+
+entry:
+ %temp_slot = alloca ptr
+ %dummy = alloca i32
+
+ ; Store and immediately reload the address of 'dummy'.
+ ; This analysis uses flow-sensitivity to prove that these memory operations
+ ; occur before the recursive branch and do not result in a capture of
+ ; the alloca's address.
+ store ptr %dummy, ptr %temp_slot
+ %val = load ptr, ptr %temp_slot
+
+ ; Check if the recursion depth has reached zero.
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+
+recurse:
+ ; Prepare arguments for the next iteration.
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+
+ ; This call should be transformed into a branch back to 'tailrecurse'.
+ ; PHI nodes will handle the state transition for %n and %acc.
+ %res = call i32 @test_alloca_reuse(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+
+exit:
+ ; Return the accumulated result.
+ ret i32 %acc
+}
diff --git a/llvm/test/Transforms/TailCallElim/alloca-temporal-alias.ll b/llvm/test/Transforms/TailCallElim/alloca-temporal-alias.ll
new file mode 100644
index 0000000000000..fef85a3fa3875
--- /dev/null
+++ b/llvm/test/Transforms/TailCallElim/alloca-temporal-alias.ll
@@ -0,0 +1,41 @@
+; RUN: opt -passes=tailcallelim -S < %s | FileCheck %s
+
+; Test that Tail Call Elimination can correctly identify when an alloca's
+; address is stored in another alloca but does not escape.
+; The analysis should recognize that if all loads from the storage alloca
+; occur before the recursive call, the original alloca remains safe for TRE.
+
+define i32 @test_temporary_alias(i32 %n, i32 %acc) {
+; CHECK-LABEL: @test_temporary_alias
+; CHECK: tailrecurse:
+entry:
+ %local_var = alloca i32
+ %ptr_storage = alloca ptr
+
+ ; Store the address of the local variable into a temporary storage alloca.
+ store ptr %local_var, ptr %ptr_storage
+
+ ; Access the stored address locally.
+ %addr = load ptr, ptr %ptr_storage
+ store i32 42, ptr %addr
+
+ ; At this point, %ptr_storage is no longer accessed.
+ ; The flow-sensitive analysis using DominatorTree should prove that
+ ; %local_var does not escape through %ptr_storage in any path reaching
+ ; the recursive call.
+
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+
+recurse:
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+
+ ; TRE should be applied here because the address of %local_var
+ ; is not captured by any instruction dominating this call.
+ %res = call i32 @test_temporary_alias(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+
+exit:
+ ret i32 %acc
+}
diff --git a/llvm/test/Transforms/TailCallElim/prevent-tre-alloca-escape.ll b/llvm/test/Transforms/TailCallElim/prevent-tre-alloca-escape.ll
new file mode 100644
index 0000000000000..3aaabea48032e
--- /dev/null
+++ b/llvm/test/Transforms/TailCallElim/prevent-tre-alloca-escape.ll
@@ -0,0 +1,45 @@
+; RUN: opt -passes=tailcallelim -S < %s | FileCheck %s
+
+; Test that Tail Call Elimination correctly identifies when an alloca's
+; address is stored in another alloca and subsequently escapes.
+; The analysis should recognize that if the stored address is loaded back
+; and passed to an opaque function, the original alloca is no longer safe
+; for TRE because its lifetime could be extended beyond the current frame.
+
+declare void @opaque(ptr)
+
+define i32 @prevent_tre_on_load_escape(i32 %n, i32 %acc) {
+; CHECK-LABEL: @prevent_tre_on_load_escape
+; CHECK-NOT: tailrecurse:
+; CHECK: %res = call i32 @prevent_tre_on_load_escape
+entry:
+ %local_var = alloca i32
+ %ptr_storage = alloca ptr
+
+ ; Store the address of the local variable into a temporary storage alloca.
+ store ptr %local_var, ptr %ptr_storage
+
+ ; Access the stored address and pass it to an external function.
+ ; This load and subsequent call make %local_var "escape".
+ %addr = load ptr, ptr %ptr_storage
+ call void @opaque(ptr %addr)
+
+ ; The flow-sensitive analysis should detect that %local_var escapes
+ ; through %ptr_storage. Any recursive call in this function must NOT
+ ; be transformed into a loop (TRE).
+
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+
+recurse:
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+
+ ; TRE must NOT be applied here because the address of %local_var
+ ; has escaped and could be accessed by @opaque during the next iterations.
+ %res = call i32 @prevent_tre_on_load_escape(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+
+exit:
+ ret i32 %acc
+}
diff --git a/llvm/test/Transforms/TailCallElim/tre-stats-improvement.ll b/llvm/test/Transforms/TailCallElim/tre-stats-improvement.ll
new file mode 100644
index 0000000000000..ee5bb265c1acf
--- /dev/null
+++ b/llvm/test/Transforms/TailCallElim/tre-stats-improvement.ll
@@ -0,0 +1,63 @@
+; REQUIRES: asserts
+; RUN: opt -passes=tailcallelim -stats -S < %s 2>&1 | FileCheck %s
+
+; Verify that the enhanced escape analysis correctly identifies non-escaping
+; allocas even when their addresses are stored in other local stack slots.
+; We expect all three functions to be optimized, incrementing the NumEliminated counter.
+
+; CHECK: 3 tailcallelim - Number of tail calls removed
+
+define i32 @func1(i32 %n, i32 %acc) {
+entry:
+ %a = alloca i32
+ %ptr = alloca ptr
+ ; The address of %a is stored in %ptr.
+ ; Our flow-sensitive analysis proves %a does not escape through %ptr
+ ; before the recursive call.
+ store ptr %a, ptr %ptr
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+recurse:
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+ %res = call i32 @func1(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+exit:
+ ret i32 %acc
+}
+
+define i32 @func2(i32 %n, i32 %acc) {
+entry:
+ %b = alloca i32
+ %ptr = alloca ptr
+ ; Transitive tracking: even with a similar pattern in func2,
+ ; the tracker should maintain precision.
+ store ptr %b, ptr %ptr
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+recurse:
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+ %res = call i32 @func2(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+exit:
+ ret i32 %acc
+}
+
+define i32 @func3(i32 %n, i32 %acc) {
+entry:
+ %c = alloca i32
+ %ptr = alloca ptr
+ ; Another instance to ensure the stats counter increments correctly
+ ; across multiple successful TRE applications.
+ store ptr %c, ptr %ptr
+ %cond = icmp eq i32 %n, 0
+ br i1 %cond, label %exit, label %recurse
+recurse:
+ %n_dec = sub i32 %n, 1
+ %acc_inc = add i32 %acc, 1
+ %res = call i32 @func3(i32 %n_dec, i32 %acc_inc)
+ ret i32 %res
+exit:
+ ret i32 %acc
+}
More information about the llvm-commits
mailing list