From f494d039112c6c310ff53e1cabe4de1888957155 Mon Sep 17 00:00:00 2001 From: Rose Date: Wed, 18 Jun 2025 16:05:44 -0400 Subject: [PATCH 1/3] [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 | 192 ++++++++++- .../ObjCARC/test_autorelease_pool.ll | 319 ++++++++++++++++++ 3 files changed, 522 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..e879b8f06e912 100644 --- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp +++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp @@ -39,6 +39,7 @@ #include "llvm/Analysis/ObjCARCAnalysisUtils.h" #include "llvm/Analysis/ObjCARCInstKind.h" #include "llvm/Analysis/ObjCARCUtil.h" +#include "llvm/Analysis/OptimizationRemarkEmitter.h" #include "llvm/IR/BasicBlock.h" #include "llvm/IR/CFG.h" #include "llvm/IR/Constant.h" @@ -132,11 +133,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 +564,8 @@ class ObjCARCOpt { void OptimizeReturns(Function &F); + void OptimizeAutoreleasePools(Function &F); + template static void cloneOpBundlesIf(CallBase *CI, SmallVectorImpl &OpBundles, @@ -2473,6 +2473,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 +2490,183 @@ bool ObjCARCOpt::run(Function &F, AAResults &AA) { return Changed; } +/// Interprocedurally determine if calls made by the given call site can +/// possibly produce autoreleases. +bool MayAutorelease(const CallBase &CB, unsigned Depth = 0) { + if (CB.onlyReadsMemory()) + return false; + + // This recursion depth limit is arbitrary. It's just great + // enough to cover known interesting testcases. + if (Depth > 5) + return true; + + if (const Function *Callee = CB.getCalledFunction()) { + if (!Callee->hasExactDefinition()) + return true; + for (const BasicBlock &BB : *Callee) { + for (const Instruction &I : BB) { + // TODO: Ignore all instructions between autorelease pools + ARCInstKind InstKind = GetBasicARCInstKind(&I); + switch (InstKind) { + case ARCInstKind::Autorelease: + case ARCInstKind::AutoreleaseRV: + case ARCInstKind::FusedRetainAutorelease: + case ARCInstKind::FusedRetainAutoreleaseRV: + case ARCInstKind::LoadWeak: + // These may produce autoreleases + return true; + + case ARCInstKind::Retain: + case ARCInstKind::RetainRV: + case ARCInstKind::UnsafeClaimRV: + case ARCInstKind::RetainBlock: + case ARCInstKind::Release: + case ARCInstKind::NoopCast: + case ARCInstKind::LoadWeakRetained: + case ARCInstKind::StoreWeak: + case ARCInstKind::InitWeak: + case ARCInstKind::MoveWeak: + case ARCInstKind::CopyWeak: + case ARCInstKind::DestroyWeak: + case ARCInstKind::StoreStrong: + case ARCInstKind::AutoreleasepoolPush: + case ARCInstKind::AutoreleasepoolPop: + // These ObjC runtime functions don't produce autoreleases + break; + + case ARCInstKind::CallOrUser: + case ARCInstKind::Call: + // For non-ObjC function calls, recursively analyze + if (MayAutorelease(cast(I), Depth + 1)) + return true; + break; + + case ARCInstKind::IntrinsicUser: + case ARCInstKind::User: + case ARCInstKind::None: + // These are not relevant for autorelease analysis + break; + } + } + } + return false; + } + + return true; +} + +/// Optimize autorelease pools by eliminating empty push/pop pairs. +void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) { + LLVM_DEBUG(dbgs() << "\n== ObjCARCOpt::OptimizeAutoreleasePools ==\n"); + + OptimizationRemarkEmitter ORE(&F); + + // Process each basic block independently. + // TODO: Can we optimize inter-block autorelease pool pairs? + // This would involve tracking autorelease pool state across blocks. + for (BasicBlock &BB : F) { + // Use a stack to track nested autorelease pools + SmallVector, 4> + PoolStack; // {push_inst, has_autorelease_in_scope} + + for (Instruction &Inst : llvm::make_early_inc_range(BB)) { + ARCInstKind Class = GetBasicARCInstKind(&Inst); + + switch (Class) { + case ARCInstKind::AutoreleasepoolPush: { + // Start tracking a new autorelease pool scope + auto *Push = cast(&Inst); + PoolStack.push_back( + {Push, false}); // {push_inst, has_autorelease_in_scope} + LLVM_DEBUG(dbgs() << "Found autorelease pool push: " << *Push << "\n"); + break; + } + + case ARCInstKind::AutoreleasepoolPop: { + auto *Pop = cast(&Inst); + + if (PoolStack.empty()) + break; + + auto &TopPool = PoolStack.back(); + CallInst *PendingPush = TopPool.first; + bool HasAutoreleaseInScope = TopPool.second; + + // Pop the stack - remove this pool scope + PoolStack.pop_back(); + + // Bail if this pop doesn't match the pending push + if (Pop->getArgOperand(0)->stripPointerCasts() != PendingPush) + break; + + // Bail if there were autoreleases in this scope + if (HasAutoreleaseInScope) + break; + + // Optimize: eliminate this empty autorelease pool pair + ORE.emit([&]() { + return OptimizationRemark(DEBUG_TYPE, "AutoreleasePoolElimination", + PendingPush) + << "eliminated empty autorelease pool pair"; + }); + + // Replace all uses of push with poison before deletion + PendingPush->replaceAllUsesWith( + PoisonValue::get(PendingPush->getType())); + + PendingPush->eraseFromParent(); + Pop->eraseFromParent(); + + Changed = true; + ++NumNoops; + break; + } + case ARCInstKind::CallOrUser: + case ARCInstKind::Call: + if (!MayAutorelease(cast(Inst))) + break; + [[fallthrough]]; + case ARCInstKind::Autorelease: + case ARCInstKind::AutoreleaseRV: + case ARCInstKind::FusedRetainAutorelease: + case ARCInstKind::FusedRetainAutoreleaseRV: + case ARCInstKind::LoadWeak: { + // Track that we have autorelease calls in the current pool scope + if (!PoolStack.empty()) { + PoolStack.back().second = true; // Set has_autorelease_in_scope = true + LLVM_DEBUG( + dbgs() + << "Found autorelease or potential autorelease in pool scope: " + << Inst << "\n"); + } + break; + } + + // Enumerate all remaining ARCInstKind cases explicitly + case ARCInstKind::Retain: + case ARCInstKind::RetainRV: + case ARCInstKind::UnsafeClaimRV: + case ARCInstKind::RetainBlock: + case ARCInstKind::Release: + case ARCInstKind::NoopCast: + case ARCInstKind::LoadWeakRetained: + case ARCInstKind::StoreWeak: + case ARCInstKind::InitWeak: + case ARCInstKind::MoveWeak: + case ARCInstKind::CopyWeak: + case ARCInstKind::DestroyWeak: + case ARCInstKind::StoreStrong: + case ARCInstKind::IntrinsicUser: + case ARCInstKind::User: + case ARCInstKind::None: + // These instruction kinds don't affect autorelease pool optimization + break; + } + } + } +} + /// @} /// 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..1238170e0e1fd --- /dev/null +++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll @@ -0,0 +1,319 @@ +; 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() +declare void @opaque_callee() + +; 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 +} + +; Test 5: Pool with opaque call should not be optimized +define void @test_opaque_call() { +; CHECK-LABEL: define void @test_opaque_call() { +; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]] +; CHECK-NEXT: call void @opaque_callee() +; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]] +; CHECK-NEXT: ret void +; + %pool = call ptr @llvm.objc.autoreleasePoolPush() + call void @opaque_callee() + call void @llvm.objc.autoreleasePoolPop(ptr %pool) + ret void +} + +; Test 6: Nested empty pools should be eliminated +define void @test_nested_empty_pools() { +; CHECK-LABEL: define void @test_nested_empty_pools() { +; CHECK-NEXT: ret void +; + %pool1 = call ptr @llvm.objc.autoreleasePoolPush() + %pool2 = call ptr @llvm.objc.autoreleasePoolPush() + call void @llvm.objc.autoreleasePoolPop(ptr %pool2) + call void @llvm.objc.autoreleasePoolPop(ptr %pool1) + ret void +} + +; Test 7: Empty pool with cast should be eliminated +define void @test_empty_pool_with_cast() { +; CHECK-LABEL: define void @test_empty_pool_with_cast() { +; CHECK-NEXT: [[CAST:%.*]] = bitcast ptr poison to ptr +; CHECK-NEXT: ret void +; + %pool = call ptr @llvm.objc.autoreleasePoolPush() + %cast = bitcast ptr %pool to ptr + call void @llvm.objc.autoreleasePoolPop(ptr %cast) + ret void +} + +; Test 8: Autorelease shadowing - autorelease in inner pool doesn't prevent outer optimization +define void @test_autorelease_shadowing_basic() { +; CHECK-LABEL: define void @test_autorelease_shadowing_basic() { +; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() +; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]] +; CHECK-NEXT: ret void +; + %obj = call ptr @create_object() + %outer_pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; Inner pool with autorelease - this should be shadowed + %inner_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj) + call void @llvm.objc.autoreleasePoolPop(ptr %inner_pool) + + call void @llvm.objc.autoreleasePoolPop(ptr %outer_pool) + ret void +} + +; Test 9: Multiple nested levels with shadowing +define void @test_multiple_nested_shadowing() { +; CHECK-LABEL: define void @test_multiple_nested_shadowing() { +; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() +; CHECK-NEXT: [[OBJ2:%.*]] = 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: ret void +; + %obj1 = call ptr @create_object() + %obj2 = call ptr @create_object() + %outer_pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; First inner pool + %inner1_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj1) + call void @llvm.objc.autoreleasePoolPop(ptr %inner1_pool) + + ; Second inner pool with nested level + %inner2_pool = call ptr @llvm.objc.autoreleasePoolPush() + %inner3_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj2) + call void @llvm.objc.autoreleasePoolPop(ptr %inner3_pool) + call void @llvm.objc.autoreleasePoolPop(ptr %inner2_pool) + + call void @llvm.objc.autoreleasePoolPop(ptr %outer_pool) + ret void +} + +; Test 10: Autorelease outside inner pool prevents optimization +define void @test_autorelease_outside_inner_pool() { +; CHECK-LABEL: define void @test_autorelease_outside_inner_pool() { +; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() +; CHECK-NEXT: [[OBJ2:%.*]] = 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: ret void +; + %obj1 = call ptr @create_object() + %obj2 = call ptr @create_object() + %outer_pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; This autorelease is NOT in an inner pool, so outer pool can't be optimized + call ptr @llvm.objc.autorelease(ptr %obj1) + + ; Inner pool with autorelease (shadowed) + %inner_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj2) + call void @llvm.objc.autoreleasePoolPop(ptr %inner_pool) + + call void @llvm.objc.autoreleasePoolPop(ptr %outer_pool) + ret void +} + +; Test 11: Known ObjC functions don't prevent optimization +define void @test_known_objc_functions() { +; CHECK-LABEL: define void @test_known_objc_functions() { +; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() +; CHECK-NEXT: ret void +; + %obj = call ptr @create_object() + %pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; These are all known ObjC runtime functions that don't produce autoreleases + %retained = call ptr @llvm.objc.retain(ptr %obj) + call void @llvm.objc.release(ptr %obj) + + call void @llvm.objc.autoreleasePoolPop(ptr %pool) + ret void +} + +; Test 12: Complex shadowing with mixed autoreleases +define void @test_complex_shadowing() { +; CHECK-LABEL: define void @test_complex_shadowing() { +; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() +; CHECK-NEXT: [[OBJ2:%.*]] = call ptr @create_object() +; 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: ret void +; + %obj1 = call ptr @create_object() + %obj2 = call ptr @create_object() + %obj3 = call ptr @create_object() + %outer_pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; This autorelease is outside inner pools - prevents optimization + call ptr @llvm.objc.autorelease(ptr %obj1) + + ; Inner pool 1 with shadowed autorelease + %inner1_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj2) + call void @llvm.objc.autoreleasePoolPop(ptr %inner1_pool) + + ; Some safe ObjC operations + %retained = call ptr @llvm.objc.retain(ptr %obj3) + call void @llvm.objc.release(ptr %retained) + + ; Inner pool 2 with shadowed autorelease + %inner2_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj3) + call void @llvm.objc.autoreleasePoolPop(ptr %inner2_pool) + + call void @llvm.objc.autoreleasePoolPop(ptr %outer_pool) + ret void +} + +; Test 13: Non-ObjC function that may autorelease prevents optimization +define void @test_non_objc_may_autorelease() { +; CHECK-LABEL: define void @test_non_objc_may_autorelease() { +; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]] +; CHECK-NEXT: [[TMP1:%.*]] = call ptr @function_that_might_autorelease() +; CHECK-NEXT: call void @llvm.objc.autoreleasePoolPop(ptr [[POOL]]) #[[ATTR0]] +; CHECK-NEXT: ret void +; + %pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @function_that_might_autorelease() + call void @llvm.objc.autoreleasePoolPop(ptr %pool) + ret void +} + +; Test 14: Non-ObjC function that doesn't autorelease allows optimization +define void @test_non_objc_no_autorelease() { +; CHECK-LABEL: define void @test_non_objc_no_autorelease() { +; CHECK-NEXT: call void @safe_function() +; CHECK-NEXT: ret void +; + %pool = call ptr @llvm.objc.autoreleasePoolPush() + call void @safe_function() + call void @llvm.objc.autoreleasePoolPop(ptr %pool) + ret void +} + +; Test 15: Incomplete push/pop pairs across blocks - only inner pairs count +define void @test_incomplete_pairs_inner_shadowing() { +; CHECK-LABEL: define void @test_incomplete_pairs_inner_shadowing() { +; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() +; CHECK-NEXT: [[OUTER_POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]] +; CHECK-NEXT: call void @llvm.objc.release(ptr [[OBJ]]) #[[ATTR0]], !clang.imprecise_release [[META0]] +; CHECK-NEXT: ret void +; + %obj = call ptr @create_object() + %outer_pool = call ptr @llvm.objc.autoreleasePoolPush() + + ; Inner complete pair - autorelease should be shadowed by this + %inner_pool = call ptr @llvm.objc.autoreleasePoolPush() + call ptr @llvm.objc.autorelease(ptr %obj) ; This SHOULD be shadowed by inner pair + call void @llvm.objc.autoreleasePoolPop(ptr %inner_pool) ; Completes the inner pair + + ; Note: %outer_pool pop is in a different block (common pattern) + ; But the autorelease was shadowed by the complete inner pair + ret void +} + +; Helper functions for testing interprocedural analysis + +; Safe function that doesn't call autorelease +define void @safe_function() { + ; Just some computation, no autoreleases +; CHECK-LABEL: define void @safe_function() { +; CHECK-NEXT: [[X:%.*]] = add i32 1, 2 +; CHECK-NEXT: ret void +; + %x = add i32 1, 2 + ret void +} + +; Function that may produce autoreleases (simulated by calling autorelease) +define ptr @function_that_might_autorelease() { +; CHECK-LABEL: define ptr @function_that_might_autorelease() { +; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() +; CHECK-NEXT: [[AUTORELEASED:%.*]] = call ptr @llvm.objc.autorelease(ptr [[OBJ]]) #[[ATTR0]] +; CHECK-NEXT: ret ptr [[AUTORELEASED]] +; + %obj = call ptr @create_object() + %autoreleased = call ptr @llvm.objc.autorelease(ptr %obj) + ret ptr %autoreleased +} + +;. +; CHECK: [[META0]] = !{} +;. From 2cf7ddab1bd41b516838e21e5be350f05f0b738f Mon Sep 17 00:00:00 2001 From: Rose Date: Wed, 16 Jul 2025 11:15:10 -0400 Subject: [PATCH 2/3] Fix concerns --- llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 2 +- .../ObjCARC/test_autorelease_pool.ll | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp index e879b8f06e912..09db464ec6a25 100644 --- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp +++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp @@ -2615,8 +2615,8 @@ void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) { PendingPush->replaceAllUsesWith( PoisonValue::get(PendingPush->getType())); - PendingPush->eraseFromParent(); Pop->eraseFromParent(); + PendingPush->eraseFromParent(); Changed = true; ++NumNoops; diff --git a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll index 1238170e0e1fd..896717f92146f 100644 --- a/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll +++ b/llvm/test/Transforms/ObjCARC/test_autorelease_pool.ll @@ -11,7 +11,7 @@ declare void @use_object(ptr) declare ptr @object_with_thing() declare void @opaque_callee() -; Test 1: Empty autorelease pool should be eliminated +; Empty autorelease pool should be eliminated define void @test_empty_pool() { ; CHECK-LABEL: define void @test_empty_pool() { ; CHECK-NEXT: ret void @@ -21,7 +21,7 @@ define void @test_empty_pool() { ret void } -; Test 2: Pool with only release should be removed +; 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() @@ -35,7 +35,7 @@ define void @test_autorelease_to_release() { ret void } -; Test 3: Pool with autoreleases should not be optimized +; 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() @@ -59,7 +59,7 @@ define void @test_multiple_autoreleases() { ret void } -; Test 4: Pool with calls should not be optimized +; 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]] @@ -75,7 +75,7 @@ define void @test_calls() { ret void } -; Test 5: Pool with opaque call should not be optimized +; Pool with opaque call should not be optimized define void @test_opaque_call() { ; CHECK-LABEL: define void @test_opaque_call() { ; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]] @@ -89,7 +89,7 @@ define void @test_opaque_call() { ret void } -; Test 6: Nested empty pools should be eliminated +; Nested empty pools should be eliminated define void @test_nested_empty_pools() { ; CHECK-LABEL: define void @test_nested_empty_pools() { ; CHECK-NEXT: ret void @@ -101,7 +101,7 @@ define void @test_nested_empty_pools() { ret void } -; Test 7: Empty pool with cast should be eliminated +; Empty pool with cast should be eliminated define void @test_empty_pool_with_cast() { ; CHECK-LABEL: define void @test_empty_pool_with_cast() { ; CHECK-NEXT: [[CAST:%.*]] = bitcast ptr poison to ptr @@ -113,7 +113,7 @@ define void @test_empty_pool_with_cast() { ret void } -; Test 8: Autorelease shadowing - autorelease in inner pool doesn't prevent outer optimization +; Autorelease shadowing - autorelease in inner pool doesn't prevent outer optimization define void @test_autorelease_shadowing_basic() { ; CHECK-LABEL: define void @test_autorelease_shadowing_basic() { ; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() @@ -132,7 +132,7 @@ define void @test_autorelease_shadowing_basic() { ret void } -; Test 9: Multiple nested levels with shadowing +; Multiple nested levels with shadowing define void @test_multiple_nested_shadowing() { ; CHECK-LABEL: define void @test_multiple_nested_shadowing() { ; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() @@ -161,7 +161,7 @@ define void @test_multiple_nested_shadowing() { ret void } -; Test 10: Autorelease outside inner pool prevents optimization +; Autorelease outside inner pool prevents optimization define void @test_autorelease_outside_inner_pool() { ; CHECK-LABEL: define void @test_autorelease_outside_inner_pool() { ; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() @@ -186,7 +186,7 @@ define void @test_autorelease_outside_inner_pool() { ret void } -; Test 11: Known ObjC functions don't prevent optimization +; Known ObjC functions don't prevent optimization define void @test_known_objc_functions() { ; CHECK-LABEL: define void @test_known_objc_functions() { ; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() @@ -203,7 +203,7 @@ define void @test_known_objc_functions() { ret void } -; Test 12: Complex shadowing with mixed autoreleases +; Complex shadowing with mixed autoreleases define void @test_complex_shadowing() { ; CHECK-LABEL: define void @test_complex_shadowing() { ; CHECK-NEXT: [[OBJ1:%.*]] = call ptr @create_object() @@ -242,7 +242,7 @@ define void @test_complex_shadowing() { ret void } -; Test 13: Non-ObjC function that may autorelease prevents optimization +; Non-ObjC function that may autorelease prevents optimization define void @test_non_objc_may_autorelease() { ; CHECK-LABEL: define void @test_non_objc_may_autorelease() { ; CHECK-NEXT: [[POOL:%.*]] = call ptr @llvm.objc.autoreleasePoolPush() #[[ATTR0]] @@ -256,7 +256,7 @@ define void @test_non_objc_may_autorelease() { ret void } -; Test 14: Non-ObjC function that doesn't autorelease allows optimization +; Non-ObjC function that doesn't autorelease allows optimization define void @test_non_objc_no_autorelease() { ; CHECK-LABEL: define void @test_non_objc_no_autorelease() { ; CHECK-NEXT: call void @safe_function() @@ -268,7 +268,7 @@ define void @test_non_objc_no_autorelease() { ret void } -; Test 15: Incomplete push/pop pairs across blocks - only inner pairs count +; Incomplete push/pop pairs across blocks - only inner pairs count define void @test_incomplete_pairs_inner_shadowing() { ; CHECK-LABEL: define void @test_incomplete_pairs_inner_shadowing() { ; CHECK-NEXT: [[OBJ:%.*]] = call ptr @create_object() From b6222252b9da2eb07887e9434f1d4ffb79d35f80 Mon Sep 17 00:00:00 2001 From: AZero13 Date: Wed, 16 Jul 2025 11:43:07 -0400 Subject: [PATCH 3/3] Update llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp Co-authored-by: Jon Roelofs --- llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp index 09db464ec6a25..66a2c7632aadc 100644 --- a/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp +++ b/llvm/lib/Transforms/ObjCARC/ObjCARCOpts.cpp @@ -2626,7 +2626,7 @@ void ObjCARCOpt::OptimizeAutoreleasePools(Function &F) { case ARCInstKind::Call: if (!MayAutorelease(cast(Inst))) break; - [[fallthrough]]; + LLVM_FALLTHROUGH; case ARCInstKind::Autorelease: case ARCInstKind::AutoreleaseRV: case ARCInstKind::FusedRetainAutorelease: