[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
Mon Jul 13 05:47:48 PDT 2026
https://github.com/AZero13 updated https://github.com/llvm/llvm-project/pull/152353
>From d4ca6cbbed483347c462e63f0f12b6f86904146b Mon Sep 17 00:00:00 2001
From: Mike-Goutokuji <gfunni234 at gmail.com>
Date: Thu, 28 May 2026 20:19:56 -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 1018d614988cd38dd64d25326730842eb3ce162c Mon Sep 17 00:00:00 2001
From: Mike-Goutokuji <gfunni234 at gmail.com>
Date: Thu, 28 May 2026 20:51:11 -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 | 101 ++++++++++++++++--
llvm/test/Transforms/ObjCARC/basic.ll | 6 +-
.../ObjCARC/test_autorelease_pool.ll | 55 ++++++++--
3 files changed, 139 insertions(+), 23 deletions(-)
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index f92e5bbdf0111..2e0c00f2d4c63 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,56 @@ 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())
+ return It->second;
+
+ BasicBlock *BB = AutoreleaseInst->getParent();
+ Instruction *Result = nullptr;
+
+ SmallVector<Instruction *, 4> ScopeSiblings;
+
+ 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;
+ } else if (Class == ARCInstKind::AutoreleasepoolPop) {
+ if (Depth == 0) {
+ Result = &*I;
+ break;
+ }
+ --Depth;
+ } else if (Class == ARCInstKind::Autorelease && Depth == 0) {
+ // It is safe to eagerly cache this pool pop for sibling autoreleases
+ // because the optimizer processes instructions strictly top-down. When
+ // it later reaches the sibling, it will find the cached pop and erase
+ // itself from the cache. Since the cache is cleared per-function,
+ // dangling pointer keys are not a concern.
+ ScopeSiblings.push_back(&*I);
+ }
+ }
+
+ FollowingPoolPopCache[AutoreleaseInst] = Result;
+ for (Instruction *I : ScopeSiblings)
+ FollowingPoolPopCache[I] = Result;
+ return Result;
+}
+
/// Turn objc_retainAutoreleasedReturnValue into objc_retain if the operand is
/// not a return value.
bool
@@ -761,6 +816,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.
@@ -978,7 +1035,7 @@ void ObjCARCOpt::OptimizeIndividualCallImpl(Function &F, Instruction *Inst,
}
// objc_autorelease(x) -> objc_release(x) if x is otherwise unused.
- if (IsAutorelease(Class) && Inst->use_empty()) {
+ if (Class == ARCInstKind::Autorelease && Inst->use_empty()) {
CallInst *Call = cast<CallInst>(Inst);
const Value *Arg = Call->getArgOperand(0);
Arg = FindSingleUseIdentifiedObject(Arg);
@@ -986,19 +1043,45 @@ 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: "
- << *Call << "\nNew: " << *NewCall << "\n");
+ LLVM_DEBUG(
+ dbgs() << "Replacing objc_autorelease(x) with objc_release(x)\n");
+
+ 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() << "Moving objc_autorelease(x) to objc_release(x) "
+ "before autoreleasePoolPop.\n");
+ Call->replaceAllUsesWith(Call->getArgOperand(0));
+ FollowingPoolPopCache.erase(Call);
EraseInstruction(Call);
Inst = NewCall;
Class = ARCInstKind::Release;
diff --git a/llvm/test/Transforms/ObjCARC/basic.ll b/llvm/test/Transforms/ObjCARC/basic.ll
index 24f2862ffe5c7..16314762253d3 100644
--- a/llvm/test/Transforms/ObjCARC/basic.ll
+++ b/llvm/test/Transforms/ObjCARC/basic.ll
@@ -1860,16 +1860,16 @@ entry:
ret void
}
-; Don't the known-incremented retain+release elimination if the pointer is
-; autoreleased and there's an autoreleasePoolPop.
+; The autorelease is moved to a release just before the autoreleasePoolPop.
+; The retain+release pair is not eliminated.
; CHECK-LABEL: define void @test43(
; CHECK-NEXT: entry:
; CHECK-NEXT: call ptr @llvm.objc.retain(ptr %p)
-; CHECK-NEXT: call ptr @llvm.objc.autorelease(ptr %p)
; CHECK-NEXT: call ptr @llvm.objc.retain
; CHECK-NEXT: call void @use_pointer(ptr %p)
; CHECK-NEXT: call void @use_pointer(ptr %p)
+; CHECK-NEXT: call void @llvm.objc.release(ptr %p) {{.*}}, !clang.imprecise_release
; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr undef)
; CHECK-NEXT: call void @llvm.objc.release
; CHECK-NEXT: ret void
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
index b5acba1f8cf89..ff7e57d88bcc1 100644
--- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -36,16 +36,17 @@ 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()
; CHECK-NEXT: [[OBJ2:%.*]] = call ptr @create_object()
; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
; 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 [[OBJ1]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
+; 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
@@ -333,11 +333,9 @@ define void @test_cross_function_inner_pool_caller() {
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: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]]
; CHECK-NEXT: ret void
;
%pool = call ptr @llvm.objc.autoreleasePoolPush()
@@ -398,6 +396,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