[llvm] [ObjCARC] Change autorelease to release when the pool state is not changed between the autorelease and the pool pop (PR #152353)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Jan 22 10:09:54 PST 2026
https://github.com/AZero13 updated https://github.com/llvm/llvm-project/pull/152353
>From bf32eaa14ca7723806336f493b4c4624a0c0a735 Mon Sep 17 00:00:00 2001
From: AZero13 <gfunni234 at gmail.com>
Date: Wed, 6 Aug 2025 14:07:16 -0400
Subject: [PATCH] [ObjCARC] Change autorelease to release when the pool state
is not changed between the autorelease and the pool pop
Prevent/mitigate quadradic scaling (O^2)
Have caching.
---
llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 165 +++++++++++++++++-
.../ObjCARC/test_autorelease_pool.ll | 6 +-
2 files changed, 159 insertions(+), 12 deletions(-)
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index 386e48f81a93f..524b0e6d682a2 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -499,6 +499,14 @@ class ObjCARCOpt {
DenseMap<BasicBlock *, ColorVector> BlockEHColors;
+ /// Cache for HasFollowingAutoreleasePoolPop results to avoid quadratic
+ /// scanning when multiple autoreleases are in the same basic block.
+ DenseMap<Instruction *, bool> AutoreleasePoolPopCache;
+
+ /// Check if there is an autoreleasePoolPop after the given autorelease
+ /// instruction in the same basic block.
+ bool HasFollowingAutoreleasePoolPop(Instruction *AutoreleaseInst);
+
bool OptimizeRetainRVCall(Function &F, Instruction *RetainRV);
void OptimizeAutoreleaseRVCall(Function &F, Instruction *AutoreleaseRV,
ARCInstKind &Class);
@@ -601,6 +609,112 @@ class ObjCARCOpt {
};
} // end anonymous namespace
+/// Check if there is an autoreleasePoolPop after the given autorelease
+/// instruction in the same basic block with no intervening calls that
+/// could affect the autorelease pool.
+bool ObjCARCOpt::HasFollowingAutoreleasePoolPop(Instruction *AutoreleaseInst) {
+ assert(IsAutorelease(GetBasicARCInstKind(AutoreleaseInst)));
+
+ // Check cache first
+ auto It = AutoreleasePoolPopCache.find(AutoreleaseInst);
+ if (It != AutoreleasePoolPopCache.end())
+ return It->second;
+
+ BasicBlock *BB = AutoreleaseInst->getParent();
+ bool Result = false;
+
+ // Instructions we've visited that we can cache the result for
+ SmallVector<Instruction *, 4> Visited;
+
+ // Look forward from the autorelease instruction
+ for (BasicBlock::iterator I = std::next(AutoreleaseInst->getIterator()),
+ E = BB->end();
+ I != E; ++I) {
+ ARCInstKind Class = GetBasicARCInstKind(&*I);
+
+ switch (Class) {
+ case ARCInstKind::AutoreleasepoolPop:
+ // Found a pool pop - the autorelease will be drained
+ Result = true;
+ goto done;
+
+ case ARCInstKind::AutoreleasepoolPush: {
+ // A nested pool started. Skip to its matching pop and continue scanning,
+ // since the nested pool doesn't affect whether the outer pool will drain
+ // our autorelease. We can skip over everything inside the nested pool
+ // (retains, releases, calls, etc.) because the nested pool is isolated.
+ int Depth = 1;
+ BasicBlock::iterator J = std::next(I);
+ for (; J != E && Depth > 0; ++J) {
+ ARCInstKind NestedClass = GetBasicARCInstKind(&*J);
+ if (NestedClass == ARCInstKind::AutoreleasepoolPush)
+ ++Depth;
+ else if (NestedClass == ARCInstKind::AutoreleasepoolPop)
+ --Depth;
+ // Everything else (retains, releases, calls, etc.) inside the nested
+ // pool can be ignored - the nested pool is isolated and doesn't affect
+ // the outer pool's behavior.
+ }
+ // If we found the matching pop, continue scanning from after it
+ if (Depth == 0) {
+ // J points to after the matching pop. Set I to J-1 so that after
+ // the loop increment, we continue from after the pop.
+ I = std::prev(J);
+ continue;
+ }
+ // Unmatched push (reached end of BB) - this autorelease won't be drained
+ Result = false;
+ goto done;
+ }
+
+ case ARCInstKind::Autorelease:
+ // This autorelease is in the same scope. It will share the same fate
+ // (drained by the same future pop, or not). Cache it.
+ Visited.push_back(&*I);
+ break;
+
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ // Unknown call could affect autorelease pool state or return autoreleased
+ // objects
+ Result = false;
+ goto done;
+ case ARCInstKind::Retain:
+ case ARCInstKind::RetainRV:
+ case ARCInstKind::UnsafeClaimRV:
+ case ARCInstKind::RetainBlock:
+ case ARCInstKind::Release:
+ case ARCInstKind::AutoreleaseRV:
+ case ARCInstKind::NoopCast:
+ case ARCInstKind::FusedRetainAutorelease:
+ case ARCInstKind::FusedRetainAutoreleaseRV:
+ case ARCInstKind::LoadWeakRetained:
+ case ARCInstKind::StoreWeak:
+ case ARCInstKind::InitWeak:
+ case ARCInstKind::LoadWeak:
+ case ARCInstKind::MoveWeak:
+ case ARCInstKind::CopyWeak:
+ case ARCInstKind::DestroyWeak:
+ case ARCInstKind::StoreStrong:
+ case ARCInstKind::IntrinsicUser:
+ case ARCInstKind::User:
+ case ARCInstKind::None:
+ // Everything else is safe to ignore:
+ break;
+ }
+ }
+
+ // Reached end of basic block without finding a pool pop
+ Result = false;
+
+done:
+ // Cache the result
+ AutoreleasePoolPopCache[AutoreleaseInst] = Result;
+ for (Instruction *I : Visited)
+ AutoreleasePoolPopCache[I] = Result;
+ return Result;
+}
+
/// Turn objc_retainAutoreleasedReturnValue into objc_retain if the operand is
/// not a return value.
bool
@@ -761,6 +875,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
+ AutoreleasePoolPopCache.clear();
// Store any delayed AutoreleaseRV intrinsics, so they can be easily paired
// with RetainRV and UnsafeClaimRV.
@@ -977,12 +1093,22 @@ void ObjCARCOpt::OptimizeIndividualCallImpl(Function &F, Instruction *Inst,
break;
}
- // objc_autorelease(x) -> objc_release(x) if x is otherwise unused.
+ // objc_autorelease(x) -> objc_release(x) if x is otherwise unused
+ // OR if this autorelease is followed by an autoreleasePoolPop.
if (IsAutorelease(Class) && Inst->use_empty()) {
CallInst *Call = cast<CallInst>(Inst);
const Value *Arg = Call->getArgOperand(0);
Arg = FindSingleUseIdentifiedObject(Arg);
- if (Arg) {
+ bool ShouldConvert = (Arg != nullptr);
+ const char *Reason = "since x is otherwise unused";
+
+ // Also convert if this autorelease is followed by a pool pop
+ if (!ShouldConvert && HasFollowingAutoreleasePoolPop(Inst)) {
+ ShouldConvert = true;
+ Reason = "since it's followed by autoreleasePoolPop";
+ }
+
+ if (ShouldConvert) {
Changed = true;
++NumAutoreleases;
@@ -996,8 +1122,8 @@ void ObjCARCOpt::OptimizeIndividualCallImpl(Function &F, Instruction *Inst,
MDNode::get(C, {}));
LLVM_DEBUG(dbgs() << "Replacing autorelease{,RV}(x) with objc_release(x) "
- "since x is otherwise unused.\nOld: "
- << *Call << "\nNew: " << *NewCall << "\n");
+ << Reason << ".\nOld: " << *Call
+ << "\nNew: " << *NewCall << "\n");
EraseInstruction(Call);
Inst = NewCall;
@@ -2504,10 +2630,35 @@ bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
if (!Callee->hasExactDefinition())
return true;
for (const BasicBlock &BB : *Callee) {
- for (const Instruction &I : BB) {
- // TODO: Ignore all instructions between autorelease pools
+ for (auto It = BB.begin(), E = BB.end(); It != E; ++It) {
+ const Instruction &I = *It;
ARCInstKind InstKind = GetBasicARCInstKind(&I);
+
switch (InstKind) {
+ case ARCInstKind::AutoreleasepoolPush: {
+ // Skip over nested autorelease pools - autoreleases inside are
+ // drained by the nested pool and don't affect whether this function
+ // may autorelease.
+ int PoolDepth = 1;
+ auto J = std::next(It);
+ for (; J != E && PoolDepth > 0; ++J) {
+ ARCInstKind NestedKind = GetBasicARCInstKind(&*J);
+ if (NestedKind == ARCInstKind::AutoreleasepoolPush)
+ ++PoolDepth;
+ else if (NestedKind == ARCInstKind::AutoreleasepoolPop)
+ --PoolDepth;
+ }
+ // If we found the matching pop, skip to after it
+ if (PoolDepth == 0)
+ It = std::prev(J); // Will be incremented by loop to point after pop
+ // Unmatched push - continue scanning (conservative)
+ break;
+ }
+
+ case ARCInstKind::AutoreleasepoolPop:
+ // Skip pop instructions (we only process them when matching a push)
+ break;
+
case ARCInstKind::Autorelease:
case ARCInstKind::AutoreleaseRV:
case ARCInstKind::FusedRetainAutorelease:
@@ -2529,8 +2680,6 @@ bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) {
case ARCInstKind::CopyWeak:
case ARCInstKind::DestroyWeak:
case ARCInstKind::StoreStrong:
- case ARCInstKind::AutoreleasepoolPush:
- case ARCInstKind::AutoreleasepoolPop:
// These ObjC runtime functions don't produce autoreleases
break;
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
index 896717f92146f..150837f922c39 100644
--- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -44,7 +44,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
;
@@ -211,9 +211,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()
More information about the llvm-commits
mailing list