Unveiling iOS AutoreleasePool Changes: TLS Magic and Compiler Optimizations
This article explores the evolution of the iOS main.m AutoreleasePool since Xcode 11, explains how Thread‑Local Storage and compiler optimizations like tail‑call elimination affect object lifetimes, and demonstrates practical code examples and assembly insights for developers seeking deeper memory‑management understanding.
AutoreleasePool in main.m
In Xcode 11 the template
main.mchanged. The previous implementation created an autorelease pool around the
UIApplicationMaincall, causing the
appDelegateClassNameobject to remain in the heap and never be released because
UIApplicationMainnever returns.
The old
mainfunction looked like:
<code>int main(int argc, char *argv[]) {
void *pool = objc_autoreleasePoolPush();
NSString *appDelegateClassName = NSStringFromClass([AppDelegate class]);
name = objc_autoreleaseReturnValue(appDelegateClassName);
name = objc_retainAutoreleasedReturnValue(appDelegateClassName);
int tmp = UIApplicationMain(argc, argv, nil, appDelegateClassName);
objc_release(appDelegateClassName);
objc_autoreleasePoolPop(pool);
return tmp;
}
</code>In reality this caused two problems:
UIApplicationMainnever returns, so the
appDelegateClassNameobject stays allocated, leading to a memory leak.
When the program terminates the system reclaims the memory, making the pool drain meaningless.
The new template separates the creation of
appDelegateClassNamefrom the
UIApplicationMaincall:
<code>int main(int argc, char *argv[]) {
NSString *appDelegateClassName;
void *pool = objc_autoreleasePoolPush();
appDelegateClassName = NSStringFromClass([AppDelegate class]);
appDelegateClassName = objc_autoreleaseReturnValue(appDelegateClassName);
appDelegateClassName = objc_retainAutoreleasedReturnValue(appDelegateClassName);
objc_release(nil);
objc_autoreleasePoolPop(pool);
int tmp = UIApplicationMain(argc, argv, nil, appDelegateClassName);
objc_storeStrong(&appDelegateClassName, nil);
return tmp;
}
</code>Even with this change the
appDelegateClassNameobject is not released until after
UIApplicationMainfinishes, which still does not happen in normal execution.
Thread Local Storage
The optimization relies on Thread‑Local Storage (TLS). On arm64,
objc_autoreleaseReturnValuechecks the return address with
__builtin_return_address(0). If the instruction at that address is the no‑op
mov fp, fp, the runtime stores a marker (value 0x1) under the key
RETURN_DISPOSITION_KEYin TLS and returns the object directly, skipping
objc_autorelease.
<code>// NSObject.mm
id objc_autoreleaseReturnValue(id obj) {
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
// objc-object.h
enum ReturnDisposition : bool { ReturnAtPlus0 = false, ReturnAtPlus1 = true };
static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) {
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}
return false;
}
static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra) {
// mov fp, fp encodes as 0xaa1d03fd
if (*(uint32_t *)ra == 0xaa1d03fd) return true;
return false;
}
</code>Similarly,
objc_retainAutoreleasedReturnValuechecks the TLS marker; if it finds the value 0x1 it returns the object without calling
objc_retain.
<code>// NSObject.mm
id objc_retainAutoreleasedReturnValue(id obj) {
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
return objc_retain(obj);
}
static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() {
ReturnDisposition d = getReturnDisposition();
setReturnDisposition(ReturnAtPlus0);
return d;
}
</code>Because of this TLS‑based optimization, factory‑created objects often bypass the autorelease pool, speeding up release.
Compiler Optimizations
When the
__weakqualifier is added, the compiler inserts extra stack operations between the two runtime calls, breaking the
callerAcceptsOptimizedReturncheck. Consequently the object is placed into the autorelease pool.
<code>// Without __weak
0x100072148: bl NSStringFromClass
0x10007214c: mov x29, x29 // mov fp, fp
0x100072150: bl objc_retainAutoreleasedReturnValue
// With __weak
0x1001da0b8: bl NSStringFromClass
0x1001da0bc: str x0, [sp, #0x8]
0x1001da0c0: b ...
0x1001da0c4: mov x29, x29 // mov fp, fp
0x1001da0c8: ldr x0, [sp, #0x8]
0x1001da0cc: bl objc_retainAutoreleasedReturnValue
</code>Adjusting the Xcode build setting Optimization Level (Build Settings → Apple Clang → Code Generation) controls whether these markers appear. Using
None[-O0](Debug) keeps the markers; higher levels (
-O2,
-Os) enable tail‑call elimination and other optimizations.
Tail Call Optimization
Tail‑call elimination is performed only in Release builds. The LLVM pass
-tailcallelimtransforms a call followed by a return into a loop, removing the extra stack frame. The pass is active for optimization levels
-O2and higher, but not for
-O0or
-O1in older clang versions.
<code>// Example of tail‑call elimination pass description
This pass transforms calls of the current function (self recursion) followed by a return instruction with a branch to the entry of the function, creating a loop.
</code>Conclusion
The article traced the changes in iOS
main.mAutoreleasePool handling, revealed how Thread‑Local Storage and compiler optimizations cooperate to avoid unnecessary autoreleases, and showed how tail‑call elimination further refines generated code. Understanding these mechanisms helps iOS developers write more efficient memory‑aware code.
GrowingIO Tech Team
The official technical account of GrowingIO, showcasing our tech innovations, experience summaries, and cutting‑edge black‑tech.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.