[llvm] [ObjCARC] Change autorelease to release when the pool state is not changed between the autorelease and the pool pop (PR #144792)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Jun 18 13:45:21 PDT 2025
https://github.com/AZero13 created https://github.com/llvm/llvm-project/pull/144792
None
>From ee2f751a2235796900ccedef63796a4231247ef8 Mon Sep 17 00:00:00 2001
From: Rose <gfunni234 at gmail.com>
Date: Wed, 18 Jun 2025 16:05:44 -0400
Subject: [PATCH 1/2] [ObjCARC] Delete empty autoreleasepools with no
autoreleases in them
Erase empty autorelease pools that have no autorelease in them
---
.../ObjCARC/ARCRuntimeEntryPoints.h | 16 +++
llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 114 +++++++++++++++++-
.../ObjCARC/test_autorelease_pool.ll | 79 ++++++++++++
3 files changed, 204 insertions(+), 5 deletions(-)
create mode 100644 llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
diff --git a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
index 3fa844eda21cf..6135c7b938a3e 100644
--- a/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
+++ b/llvm/lib/Transforms/ObjCARC/ARCRuntimeEntryPoints.h
@@ -46,6 +46,8 @@ enum class ARCRuntimeEntryPointKind {
UnsafeClaimRV,
RetainAutorelease,
RetainAutoreleaseRV,
+ AutoreleasePoolPush,
+ AutoreleasePoolPop,
};
/// Declarations for ObjC runtime functions and constants. These are initialized
@@ -67,6 +69,8 @@ class ARCRuntimeEntryPoints {
UnsafeClaimRV = nullptr;
RetainAutorelease = nullptr;
RetainAutoreleaseRV = nullptr;
+ AutoreleasePoolPush = nullptr;
+ AutoreleasePoolPop = nullptr;
}
Function *get(ARCRuntimeEntryPointKind kind) {
@@ -101,6 +105,12 @@ class ARCRuntimeEntryPoints {
case ARCRuntimeEntryPointKind::RetainAutoreleaseRV:
return getIntrinsicEntryPoint(RetainAutoreleaseRV,
Intrinsic::objc_retainAutoreleaseReturnValue);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPush:
+ return getIntrinsicEntryPoint(AutoreleasePoolPush,
+ Intrinsic::objc_autoreleasePoolPush);
+ case ARCRuntimeEntryPointKind::AutoreleasePoolPop:
+ return getIntrinsicEntryPoint(AutoreleasePoolPop,
+ Intrinsic::objc_autoreleasePoolPop);
}
llvm_unreachable("Switch should be a covered switch.");
@@ -143,6 +153,12 @@ class ARCRuntimeEntryPoints {
/// Declaration for objc_retainAutoreleaseReturnValue().
Function *RetainAutoreleaseRV = nullptr;
+ /// Declaration for objc_autoreleasePoolPush().
+ Function *AutoreleasePoolPush = nullptr;
+
+ /// Declaration for objc_autoreleasePoolPop().
+ Function *AutoreleasePoolPop = nullptr;
+
Function *getIntrinsicEntryPoint(Function *&Decl, Intrinsic::ID IntID) {
if (Decl)
return Decl;
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index 5eb3f51d38945..a361585f03326 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -132,11 +132,8 @@ static const Value *FindSingleUseIdentifiedObject(const Value *Arg) {
//
// The second retain and autorelease can be deleted.
-// TODO: It should be possible to delete
-// objc_autoreleasePoolPush and objc_autoreleasePoolPop
-// pairs if nothing is actually autoreleased between them. Also, autorelease
-// calls followed by objc_autoreleasePoolPop calls (perhaps in ObjC++ code
-// after inlining) can be turned into plain release calls.
+// 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
@@ -566,6 +563,8 @@ class ObjCARCOpt {
void OptimizeReturns(Function &F);
+ void OptimizeAutoreleasePools(Function &F);
+
template <typename PredicateT>
static void cloneOpBundlesIf(CallBase *CI,
SmallVectorImpl<OperandBundleDef> &OpBundles,
@@ -2473,6 +2472,11 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
(1 << unsigned(ARCInstKind::AutoreleaseRV))))
OptimizeReturns(F);
+ // Optimizations for autorelease pools.
+ if (UsedInThisFunction & ((1 << unsigned(ARCInstKind::AutoreleasepoolPush)) |
+ (1 << unsigned(ARCInstKind::AutoreleasepoolPop))))
+ OptimizeAutoreleasePools(F);
+
// Gather statistics after optimization.
#ifndef NDEBUG
if (AreStatisticsEnabled()) {
@@ -2485,6 +2489,106 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) {
return Changed;
}
+/// Optimize autorelease pools by eliminating empty push/pop pairs.
+void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) {
+ LLVM_DEBUG(dbgs() << "\n== ObjCARCOpt::OptimizeAutoreleasePools ==\n");
+
+ // Track empty autorelease pool push/pop pairs
+ SmallVector<std::pair<CallInst *, CallInst *>, 4> EmptyPoolPairs;
+
+ // Process each basic block independently for now (can be extended to
+ // inter-block later)
+ for (BasicBlock &BB : F) {
+ CallInst *PendingPush = nullptr;
+ bool HasAutoreleaseInScope = false;
+
+ for (Instruction &Inst : BB) {
+ ARCInstKind Class = GetBasicARCInstKind(&Inst);
+
+ switch (Class) {
+ case ARCInstKind::AutoreleasepoolPush: {
+ // Start tracking a new autorelease pool scope
+ PendingPush = cast<CallInst>(&Inst);
+ HasAutoreleaseInScope = false;
+ LLVM_DEBUG(dbgs() << "Found autorelease pool push: " << *PendingPush
+ << "\n");
+ break;
+ }
+
+ case ARCInstKind::AutoreleasepoolPop: {
+ CallInst *Pop = cast<CallInst>(&Inst);
+
+ if (PendingPush) {
+ // Check if this pop matches the pending push by comparing the token
+ Value *PopArg = Pop->getArgOperand(0);
+ bool IsMatchingPop = (PopArg == PendingPush);
+
+ // Also handle bitcast case
+ if (!IsMatchingPop && isa<BitCastInst>(PopArg)) {
+ Value *BitcastSrc = cast<BitCastInst>(PopArg)->getOperand(0);
+ IsMatchingPop = (BitcastSrc == PendingPush);
+ }
+
+ if (IsMatchingPop && !HasAutoreleaseInScope) {
+ LLVM_DEBUG(dbgs() << "Eliminating empty autorelease pool pair: "
+ << *PendingPush << " and " << *Pop << "\n");
+
+ // Store the pair for careful deletion later
+ EmptyPoolPairs.push_back({PendingPush, Pop});
+
+ Changed = true;
+ ++NumNoops;
+ }
+ }
+
+ PendingPush = nullptr;
+ HasAutoreleaseInScope = false;
+ break;
+ }
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ case ARCInstKind::Autorelease:
+ case ARCInstKind::AutoreleaseRV: {
+ // Track that we have autorelease calls in the current pool scope
+ if (PendingPush) {
+ HasAutoreleaseInScope = true;
+ LLVM_DEBUG(
+ dbgs()
+ << "Found autorelease or potiential autorelease in pool scope: "
+ << Inst << "\n");
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+
+ // Handle empty pool pairs carefully to avoid use-after-delete
+ SmallVector<CallInst *, 8> DeadInsts;
+ for (auto &Pair : EmptyPoolPairs) {
+ CallInst *Push = Pair.first;
+ CallInst *Pop = Pair.second;
+
+ // Replace the pop's argument with undef before deleting the push
+ Value *UndefToken = UndefValue::get(Push->getType());
+ Pop->setArgOperand(0, UndefToken);
+
+ LLVM_DEBUG(dbgs() << "Erasing empty pool pair: " << *Push << " and " << *Pop
+ << "\n");
+ DeadInsts.push_back(Pop);
+ DeadInsts.push_back(Push);
+ }
+
+ // Remove dead instructions
+ for (CallInst *DeadInst : DeadInsts) {
+ LLVM_DEBUG(dbgs() << "Erasing dead instruction: " << *DeadInst << "\n");
+ DeadInst->eraseFromParent();
+ }
+}
+
/// @}
///
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
new file mode 100644
index 0000000000000..a848262b6d009
--- /dev/null
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -0,0 +1,79 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; Test for autorelease pool optimizations
+; RUN: opt -passes=objc-arc < %s -S | FileCheck %s
+
+declare ptr @llvm.objc.autoreleasePoolPush()
+declare void @llvm.objc.autoreleasePoolPop(ptr)
+declare ptr @llvm.objc.autorelease(ptr)
+declare ptr @llvm.objc.retain(ptr)
+declare ptr @create_object()
+declare void @use_object(ptr)
+declare ptr @object_with_thing()
+
+; Test 1: Empty autorelease pool should be eliminated
+define void @test_empty_pool() {
+; CHECK-LABEL: define void @test_empty_pool() {
+; CHECK-NEXT: ret void
+;
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
+; Test 2: Pool with only release should be removed
+define void @test_autorelease_to_release() {
+; CHECK-LABEL: define void @test_autorelease_to_release() {
+; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object()
+; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0:[0-9]+]], !clang.imprecise_release [[META0:![0-9]+]]
+; CHECK-NEXT: ret void
+;
+ %obj = call ptr @create_object()
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ call ptr @llvm.objc.autorelease(ptr %obj)
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
+; Test 3: Pool with autoreleases should not be optimized
+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.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]]
+; CHECK-NEXT: ret void
+;
+ %obj1 = call ptr @create_object()
+ %obj2 = call ptr @create_object()
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ call void @use_object(ptr %obj1)
+ call ptr @llvm.objc.autorelease(ptr %obj1)
+ call void @use_object(ptr %obj2)
+ call ptr @llvm.objc.autorelease(ptr %obj2)
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
+; Test 4: Pool with calls should not be optimized
+define void @test_calls() {
+; CHECK-LABEL: define void @test_calls() {
+; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]]
+; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @object_with_thing()
+; CHECK-NEXT: call void @use_object(ptr [[OBJ1]])
+; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]]
+; CHECK-NEXT: ret void
+;
+ %pool = call ptr @llvm.objc.autoreleasePoolPush()
+ %obj1 = call ptr @object_with_thing()
+ call void @use_object(ptr %obj1)
+ call void @llvm.objc.autoreleasePoolPop(ptr %pool)
+ ret void
+}
+
+;.
+; CHECK: [[META0]] = !{}
+;.
>From 44b1bcb3b553edb9dce9d3269c250547fcb12b1a Mon Sep 17 00:00:00 2001
From: Rose <gfunni234 at gmail.com>
Date: Wed, 18 Jun 2025 16:44:19 -0400
Subject: [PATCH 2/2] [ObjCARC] Change autorelease to release when the pool
state is not changed between the autorelease and the pool pop
---
llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 56 +++++++++++++++++--
.../ObjCARC/test_autorelease_pool.ll | 2 +-
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
index a361585f03326..3a84e6ad252e4 100644
--- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
+++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp
@@ -120,6 +120,44 @@ static const Value *FindSingleUseIdentifiedObject(const Value *Arg) {
/// \defgroup ARCOpt ARC Optimization.
/// @{
+/// 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.
+static bool HasFollowingAutoreleasePoolPop(Instruction *AutoreleaseInst) {
+ BasicBlock *BB = AutoreleaseInst->getParent();
+
+ // 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
+ return true;
+
+ case ARCInstKind::AutoreleasepoolPush:
+ // A new pool started - this autorelease won't be drained by a later pop
+ return false;
+
+ case ARCInstKind::CallOrUser:
+ case ARCInstKind::Call:
+ // Unknown call could affect autorelease pool state or return autoreleased
+ // objects
+ return false;
+
+ default:
+ // Known ObjC runtime calls and other instructions are safe to continue
+ // through
+ break;
+ }
+ }
+
+ // Reached end of basic block without finding a pool pop
+ return false;
+}
+
// TODO: On code like this:
//
// objc_retain(%x)
@@ -977,12 +1015,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 +1044,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;
diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
index a848262b6d009..40b2087b5d896 100644
--- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
+++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll
@@ -43,7 +43,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
;
More information about the llvm-commits
mailing list