Mobile Development 14 min read

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.

GrowingIO Tech Team
GrowingIO Tech Team
GrowingIO Tech Team
Unveiling iOS AutoreleasePool Changes: TLS Magic and Compiler Optimizations

AutoreleasePool in main.m

In Xcode 11 the template

main.m

changed. The previous implementation created an autorelease pool around the

UIApplicationMain

call, causing the

appDelegateClassName

object to remain in the heap and never be released because

UIApplicationMain

never returns.

The old

main

function 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:

UIApplicationMain

never returns, so the

appDelegateClassName

object 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

appDelegateClassName

from the

UIApplicationMain

call:

<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(&amp;appDelegateClassName, nil);
  return tmp;
}
</code>

Even with this change the

appDelegateClassName

object is not released until after

UIApplicationMain

finishes, which still does not happen in normal execution.

Thread Local Storage

The optimization relies on Thread‑Local Storage (TLS). On arm64,

objc_autoreleaseReturnValue

checks 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_KEY

in 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_retainAutoreleasedReturnValue

checks 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

__weak

qualifier is added, the compiler inserts extra stack operations between the two runtime calls, breaking the

callerAcceptsOptimizedReturn

check. 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

-tailcallelim

transforms a call followed by a return into a loop, removing the extra stack frame. The pass is active for optimization levels

-O2

and higher, but not for

-O0

or

-O1

in 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.m

AutoreleasePool 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.

iOSCompiler OptimizationAutoreleasePoolThread Local StorageTail Call Elimination
GrowingIO Tech Team
Written by

GrowingIO Tech Team

The official technical account of GrowingIO, showcasing our tech innovations, experience summaries, and cutting‑edge black‑tech.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.