[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