[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 07:22:28 PDT 2026


https://github.com/AZero13 updated https://github.com/llvm/llvm-project/pull/152353

>From 9170d7093941d76a6225310529010d51d7f5ee9c 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 a52baa5cdc76d426ae8faa68fe4ffc29bb4f2f1d 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   | 102 ++++++++++++++++--
 llvm/test/Transforms/ObjCARC/basic.ll         |   6 +-
 .../ObjCARC/test_autorelease_pool.ll          |  55 ++++++++--
 3 files changed, 140 insertions(+), 23 deletions(-)

diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index f92e5bbdf0111..f940afac481d7 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,57 @@ 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) {
+      // Eagerly cache the pool pop for sibling autoreleases at the same depth.
+      // If a sibling is later consumed by a different transform (e.g., the
+      // "otherwise unused" path) without querying the cache, the stale entry
+      // is harmless: the cache is keyed by raw pointer and cleared per
+      // function, and no new Autorelease instructions are created by this pass
+      // that could alias a freed key.
+      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 +817,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 +1036,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 +1044,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