[llvm] [ObjCARC] Move autorelease-to-release conversion to pool pop site instead of converting in place (PR #152353)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 18 08:57:09 PDT 2026
https://github.com/AZero13 updated https://github.com/llvm/llvm-project/pull/152353
>From 9eea13315f288de1fc8cc614a5f26d67c5b5c79c Mon Sep 17 00:00:00 2001
From: AZero13 <gfunni234 at gmail.com>
Date: Mon, 13 Jul 2026 08:52:35 -0400
Subject: [PATCH 1/2] Pre-commit tests (NFC)
---
.../ObjCARC/test_autorelease_pool.ll | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
index a25d85a76d1b0..b5acba1f8cf89 100644
--- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -5,6 +5,7 @@
declare ptr @llvm.objc.autoreleasePoolPush()
declare void @llvm.objc.autoreleasePoolPop(ptr)
declare ptr @llvm.objc.autorelease(ptr)
+declare ptr @llvm.objc.autoreleaseReturnValue(ptr)
declare ptr @llvm.objc.retain(ptr)
declare ptr @create_object()
declare void @use_object(ptr)
@@ -328,6 +329,27 @@ define void @test_cross_function_inner_pool_caller() {
ret void
}
+; Function that uses autoreleaseRV instead of autorelease
+define void @test_autoreleaseRV_optimization(ptr %obj) {
+; CHECK-LABEL: define void @test_autoreleaseRV_optimization(
+; CHECK-SAME: ptr [[OBJ:%.*]]) {
+; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
+; CHECK-NEXT: [[TMP1:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ]]) #[[ATTR0]]
+; CHECK-NEXT: [[TMP2:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ]]) #[[ATTR0]]
+; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]]
+; CHECK-NEXT: ret void
+;
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+
+ %1 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
+ %2 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
+ %3 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
+
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
define void @test_cross_function_inner_pool_callee() {
; CHECK-LABEL: define void @test_cross_function_inner_pool_callee() {
; CHECK-NEXT: [[INNER_POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
>From 585aa701aa4923fbc59e6e4c941f3246355fd4fc Mon Sep 17 00:00:00 2001
From: AZero13 <gfunni234 at gmail.com>
Date: Mon, 13 Jul 2026 08:52:51 -0400
Subject: [PATCH 2/2] [ObjCARC] Move autorelease-to-release conversion to pool
pop site instead of converting in place
Instead of converting objc_autorelease(x) to objc_release(x) in place (which requires checking for uses of x between the autorelease and the pool pop), insert the release just before the matching autoreleasePoolPop. This is safe because objc_autorelease does not change the refcount; it only registers the object for a deferred release at pool drain time. Removing the autorelease and placing an explicit release at the drain point is semantically equivalent.
---
llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 113 ++++++++++++++++--
llvm/test/Transforms/ObjCARC/basic.ll | 2 +-
.../ObjCARC/test_autorelease_pool.ll | 69 ++++++++---
3 files changed, 157 insertions(+), 27 deletions(-)
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index f92e5bbdf0111..f626ceca12cc7 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -133,9 +133,6 @@ static const Value *FindSingleUseIdentifiedObject(const Value *Arg) {
//
// The second retain and autorelease can be deleted.
-// TODO: Autorelease calls followed by objc_autoreleasePoolPop calls (perhaps in
-// ObjC++ code after inlining) can be turned into plain release calls.
-
// TODO: Critical-edge splitting. If the optimial insertion point is
// a critical edge, the current algorithm has to fail, because it doesn't
// know how to split edges. It should be possible to make the optimizer
@@ -499,6 +496,14 @@ class ObjCARCOpt {
DenseMap<BasicBlock *, ColorVector> BlockEHColors;
+ /// Cache mapping autorelease instructions to their following
+ /// autoreleasePoolPop in the same basic block (or nullptr if none).
+ DenseMap<Instruction *, Instruction *> FollowingPoolPopCache;
+
+ /// Find the autoreleasePoolPop that will drain the given autorelease
+ /// instruction in the same basic block, skipping nested pools.
+ Instruction *FindFollowingAutoreleasePoolPop(Instruction *AutoreleaseInst);
+
bool OptimizeRetainRVCall(Function &F, Instruction *RetainRV);
void OptimizeAutoreleaseRVCall(Function &F, Instruction *AutoreleaseRV,
ARCInstKind &Class);
@@ -601,6 +606,66 @@ class ObjCARCOpt {
};
} // end anonymous namespace
+/// Find the autoreleasePoolPop that will drain the given autorelease
+/// instruction in the same basic block, skipping over nested pools.
+///
+/// Since objc_autorelease does not change the refcount (it only registers the
+/// object for a deferred release at pool drain), we can move the release to
+/// just before the pool pop instead of converting in place. This avoids the
+/// need to check for uses of the pointer between the autorelease and the pop.
+Instruction *
+ObjCARCOpt::FindFollowingAutoreleasePoolPop(Instruction *AutoreleaseInst) {
+ assert(GetBasicARCInstKind(AutoreleaseInst) == ARCInstKind::Autorelease);
+
+ auto It = FollowingPoolPopCache.find(AutoreleaseInst);
+ if (It != FollowingPoolPopCache.end()) {
+ // Note: The cached value is a raw pointer to a pool pop. This is safe
+ // because this pass never erases autoreleasePoolPop instructions.
+ return It->second;
+ }
+
+ BasicBlock *BB = AutoreleaseInst->getParent();
+
+ SmallVector<SmallVector<Instruction *, 2>, 4> AutoreleasesByDepth(1);
+ AutoreleasesByDepth[0].push_back(AutoreleaseInst);
+
+ int Depth = 0;
+ for (BasicBlock::iterator I = std::next(AutoreleaseInst->getIterator()),
+ E = BB->end();
+ I != E; ++I) {
+ ARCInstKind Class = GetBasicARCInstKind(&*I);
+
+ if (Class == ARCInstKind::AutoreleasepoolPush) {
+ ++Depth;
+ if (Depth == (int)AutoreleasesByDepth.size())
+ AutoreleasesByDepth.emplace_back();
+ else
+ assert(AutoreleasesByDepth[Depth].empty() &&
+ "reused bucket must be empty");
+ } else if (Class == ARCInstKind::AutoreleasepoolPop) {
+ for (Instruction *J : AutoreleasesByDepth[Depth])
+ FollowingPoolPopCache[J] = &*I;
+ AutoreleasesByDepth[Depth].clear();
+ if (Depth == 0)
+ return &*I;
+ --Depth;
+ } else if (Class == ARCInstKind::Autorelease) {
+ AutoreleasesByDepth[Depth].push_back(&*I);
+ } else if (Class == ARCInstKind::Call || Class == ARCInstKind::CallOrUser) {
+ // An opaque call can push or pop an autorelease pool, which dynamically
+ // changes the pool stack. We cannot rely on the syntactic scan anymore.
+ // Break out and cache the nullptr result for all accumulated
+ // autoreleases.
+ break;
+ }
+ }
+
+ for (const auto &Autoreleases : AutoreleasesByDepth)
+ for (Instruction *I : Autoreleases)
+ FollowingPoolPopCache[I] = nullptr;
+ return nullptr;
+}
+
/// Turn objc_retainAutoreleasedReturnValue into objc_retain if the operand is
/// not a return value.
bool
@@ -761,6 +826,8 @@ void ObjCARCOpt::OptimizeIndividualCalls(Function &F) {
LLVM_DEBUG(dbgs() << "\n== ObjCARCOpt::OptimizeIndividualCalls ==\n");
// Reset all the flags in preparation for recomputing them.
UsedInThisFunction = 0;
+ // Clear the autorelease pool pop cache for this function
+ FollowingPoolPopCache.clear();
// Store any delayed AutoreleaseRV intrinsics, so they can be easily paired
// with RetainRV and UnsafeClaimRV.
@@ -986,19 +1053,49 @@ void ObjCARCOpt::OptimizeIndividualCallImpl(Function &F, Instruction *Inst,
Changed = true;
++NumAutoreleases;
- // Create the declaration lazily.
LLVMContext &C = Inst->getContext();
-
Function *Decl = EP.get(ARCRuntimeEntryPointKind::Release);
CallInst *NewCall = CallInst::Create(Decl, Call->getArgOperand(0), "",
Call->getIterator());
NewCall->setMetadata(MDKindCache.get(ARCMDKindID::ImpreciseRelease),
MDNode::get(C, {}));
- LLVM_DEBUG(dbgs() << "Replacing autorelease{,RV}(x) with objc_release(x) "
- "since x is otherwise unused.\nOld: "
+ LLVM_DEBUG(
+ dbgs() << "Replacing objc_autorelease(x) with objc_release(x)\n");
+
+ FollowingPoolPopCache.erase(Call);
+ EraseInstruction(Call);
+ Inst = NewCall;
+ Class = ARCInstKind::Release;
+ }
+ }
+
+ // objc_autorelease(x) -> objc_release(x) moved to just before the
+ // autoreleasePoolPop. Since autorelease only registers a deferred release
+ // at pool drain time without changing the refcount, placing the release at
+ // the drain point is semantically equivalent and avoids use-after-free
+ // concerns with in-place conversion.
+ if (Class == ARCInstKind::Autorelease) {
+ if (Instruction *PoolPop = FindFollowingAutoreleasePoolPop(Inst)) {
+ CallInst *Call = cast<CallInst>(Inst);
+ Changed = true;
+ ++NumAutoreleases;
+
+ LLVMContext &C = Inst->getContext();
+ Function *Decl = EP.get(ARCRuntimeEntryPointKind::Release);
+ CallInst *NewCall = CallInst::Create(Decl, Call->getArgOperand(0), "",
+ PoolPop->getIterator());
+ NewCall->setMetadata(MDKindCache.get(ARCMDKindID::ImpreciseRelease),
+ MDNode::get(C, {}));
+
+ LLVM_DEBUG(dbgs() << "Converting autorelease to release before pool pop."
+ "\nOld: "
<< *Call << "\nNew: " << *NewCall << "\n");
+ assert(Call->getType() == Call->getArgOperand(0)->getType() &&
+ "objc_autorelease result and argument types must match");
+ Call->replaceAllUsesWith(Call->getArgOperand(0));
+ FollowingPoolPopCache.erase(Call);
EraseInstruction(Call);
Inst = NewCall;
Class = ARCInstKind::Release;
@@ -1163,6 +1260,7 @@ void ObjCARCOpt::OptimizeIndividualCallImpl(Function &F, Instruction *Inst,
}
// Erase the original call.
LLVM_DEBUG(dbgs() << "Erasing: " << *CInst << "\n");
+ FollowingPoolPopCache.erase(CInst);
EraseInstruction(CInst);
} while (!Worklist.empty());
}
@@ -2368,6 +2466,7 @@ void ObjCARCOpt::OptimizeReturns(Function &F) {
LLVM_DEBUG(dbgs() << "Erasing: " << *Retain << "\nErasing: " << *Autorelease
<< "\n");
BundledInsts->eraseInst(Retain);
+ FollowingPoolPopCache.erase(Autorelease);
EraseInstruction(Autorelease);
}
}
diff --git a/llvm/test/Transforms/ObjCARC/basic.ll b/llvm/test/Transforms/ObjCARC/basic.ll
index 24f2862ffe5c7..df75c67c92d20 100644
--- a/llvm/test/Transforms/ObjCARC/basic.ll
+++ b/llvm/test/Transforms/ObjCARC/basic.ll
@@ -1860,7 +1860,7 @@ entry:
ret void
}
-; Don't the known-incremented retain+release elimination if the pointer is
+; Don't do the known-incremented retain+release elimination if the pointer is
; autoreleased and there's an autoreleasePoolPop.
; CHECK-LABEL: define void @test43(
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
index b5acba1f8cf89..7475f6747503c 100644
--- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -36,7 +36,8 @@ define void @test_autorelease_to_release() {
ret void
}
-; Pool with autoreleases should not be optimized
+; Autoreleases are converted to releases before the pool pop.
+; Pool is kept because use_object may autorelease.
define void @test_multiple_autoreleases() {
; CHECK-LABEL: define void @test_multiple_autoreleases() {
; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object()
@@ -45,7 +46,7 @@ define void @test_multiple_autoreleases() {
; CHECK-NEXT: call void @use_object(ptr [[OBJ1]])
; CHECK-NEXT: [[TMP1:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ1]]) #[[ATTR0]]
; CHECK-NEXT: call void @use_object(ptr [[OBJ2]])
-; CHECK-NEXT: [[TMP2:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ2]]) #[[ATTR0]]
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ2]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]]
; CHECK-NEXT: ret void
;
@@ -212,9 +213,7 @@ define void @test_complex_shadowing() {
; CHECK-NEXT: [[OBJ3:%.*]] = call ptr @create_object()
; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ1]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ2]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
-; CHECK-NEXT: [[INNER2_POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
-; CHECK-NEXT: [[TMP1:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ3]]) #[[ATTR0]]
-; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[INNER2_POOL]]) #[[ATTR0]]
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ3]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
; CHECK-NEXT: ret void
;
%obj1 = call ptr @create_object()
@@ -222,7 +221,8 @@ define void @test_complex_shadowing() {
%obj3 = call ptr @create_object()
%outer_pool = call ptr @llvm.objc.autoreleasePoolPush()
- ; This autorelease is outside inner pools - prevents optimization
+ ; This autorelease is outside inner pools, but is converted to a release
+ ; before the outer pool pop, so the outer pool can still be optimized.
call ptr @llvm.objc.autorelease(ptr %obj1)
; Inner pool 1 with shadowed autorelease
@@ -329,25 +329,21 @@ define void @test_cross_function_inner_pool_caller() {
ret void
}
-; Function that uses autoreleaseRV instead of autorelease
-define void @test_autoreleaseRV_optimization(ptr %obj) {
-; CHECK-LABEL: define void @test_autoreleaseRV_optimization(
+; Demonstrate that autoreleaseRV is not incorrectly converted to a deferred
+; release. Converting autoreleaseRV to release before pool pop would prevent
+; the retain/autoreleaseRV pairing optimization from eliminating the pair.
+define ptr @test_retainRV_autoreleaseRV_pairing_in_pool(ptr %obj) {
+; CHECK-LABEL: define ptr @test_retainRV_autoreleaseRV_pairing_in_pool(
; CHECK-SAME: ptr [[OBJ:%.*]]) {
-; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
-; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
-; CHECK-NEXT: [[TMP1:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ]]) #[[ATTR0]]
-; CHECK-NEXT: [[TMP2:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ]]) #[[ATTR0]]
-; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]]
-; CHECK-NEXT: ret void
+; CHECK-NEXT: ret ptr [[OBJ]]
;
%pool = call ptr @llvm.objc.autoreleasePoolPush()
- %1 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
- %2 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
- %3 = call ptr @llvm.objc.autoreleaseReturnValue(ptr %obj)
+ %retain = call ptr @llvm.objc.retain(ptr %obj)
+ %autorelease = call ptr @llvm.objc.autoreleaseReturnValue(ptr %retain)
call void @llvm.objc.autoreleasePoolPop(ptr %pool)
- ret void
+ ret ptr %autorelease
}
define void @test_cross_function_inner_pool_callee() {
@@ -398,6 +394,41 @@ define void @test_exotic_cast_bailout() {
ret void
}
+; Use of autoreleased object between autorelease and pool pop is safe because
+; the release is moved to just before the pool pop, not converted in place.
+define void @test_use_between_autorelease_and_pop(ptr %obj) {
+; CHECK-LABEL: define void @test_use_between_autorelease_and_pop(
+; CHECK-SAME: ptr [[OBJ:%.*]]) {
+; CHECK-NEXT: [[VAL:%.*]] = load i32, ptr [[OBJ]], align 4
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
+; CHECK-NEXT: ret void
+;
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ call ptr @llvm.objc.autorelease(ptr %obj)
+ ; This load uses %obj - safe because the release is placed before the pool pop,
+ ; after this load.
+ %val = load i32, ptr %obj
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
+; Use of a DIFFERENT object between autorelease and pool pop should still
+; allow the optimization.
+define void @test_use_of_different_ptr_allows_optimization(ptr %obj1, ptr %obj2) {
+; CHECK-LABEL: define void @test_use_of_different_ptr_allows_optimization(
+; CHECK-SAME: ptr [[OBJ1:%.*]], ptr [[OBJ2:%.*]]) {
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ1]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
+; CHECK-NEXT: [[VAL:%.*]] = load i32, ptr [[OBJ2]], align 4
+; CHECK-NEXT: ret void
+;
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ call ptr @llvm.objc.autorelease(ptr %obj1)
+ ; This load uses %obj2, not %obj1, so it's safe to convert the autorelease.
+ %val = load i32, ptr %obj2
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
;.
; CHECK: [[META0]] = !{}
;.
More information about the llvm-commits
mailing list