Autorelease 的返回值优化
背景
Revisit iOS Autorelease 之不经意间可能被影响的优化
Revisit iOS Autorelease之二
看 wuziqi 大神的文章,感到实在 tql。行文比较跳跃,缺乏背景知识很难看懂在说什么。
把文章谈到的问题简单化:
- 他做了一个线程池,有线程是不会退出的
- 估计是内存在持续增长,查看内存发现有 autorelease content 的内存没被释放掉
- 通过调用栈看到是有函数调用了 autoreleaseNoPage 相关的 api 创建了 autorelease content
- 本来应该被 callerAcceptsFastAutorelease 优化成 tls 的失效了,转到非优化路径下的 autorelease 模式
- 由于 callerAcceptsFastAutorelease 优化是基于指令识别,但是由于 llvm 插入了栈溢出保护的指令,导致识别不了了。
然后文章的重点就转到了
- 写个 Demo 验证 FastAutorelease 在现有 ARC 依然是有效的
- 把 Demo 改的更复杂一些,使得优化失效
怎么改的导致指令识别失效呢?就是包裹在一段 for 循环中的时候。
答案可能出乎你的意料,for会影响这个autorelease优化逻辑。
看来在没有 runloop 的线程中使用 for 循环,显式用 autorelease 包一下是最好的。
理由:
- 线程能退出,减小内存压力,thread 销毁的时候进了 autorelease 的东西会被一起销毁
- 线程不退出,像他讲的这种情况,也能避免。通过“局部 autorelease drain”销毁
下面记录更多背景知识
- autorelease 和 tls 的瓜葛
- callerAcceptsFastAutorelease 的简单分析
autorelease 和 tls 的瓜葛
而autoreleaseNoPage其实本质上就是在当前线程没有autoreleasePage的时候,创建一个。然后通过Thread Local Storage存入线程相关上下文中。
TLS 之前也写过,当时那篇文章里也简单提了下这里的 autorelease 的优化 TLS 的原理
原理就是以当前 thread 为 key 往一个全局 map 中写值。每个 thread 对应一份自己的副本,看起来 thread 是独享自己的那份存储空间一样。
多线程读写共享变量还有个处理方法就是加锁,一个线程持有锁的时期另外一个线程想访问这个共享变量要么空转等待要么休眠,总之需要等待。这就是牺牲时间。TLS 多线程读写共享变量的时候不需要任何等待直接读写,因为全局的 map 是共享的,但是内部每个 k-v 并没有被共享。牺牲的是空间。
AutoreleasePool 是个容器,通过双向链表组织。表本身存在哪里呢?其实在看文章的这几天以前我也从来没想过这个问题。表肯定是存在内存里的,表头指针的索引保存在哪里?保存在 TLS 里。
Mike Ash 文章里有提到过,文章链接 Friday Q&A 2011-09-02: Let’s Build NSAutoreleasePool
+ (CFMutableArrayRef)_threadPoolStack
{
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSString *key = @"MAAutoreleasePool thread-local pool stack";
CFMutableArrayRef array = (CFMutableArrayRef)[threadDictionary objectForKey: key];
if(!array)
{
array = CFArrayCreateMutable(NULL, 0, NULL);
[threadDictionary setObject: (id)array forKey: key];
CFRelease(array);
}
return array;
}
还是再看看 objc 源码怎么实现的
autoreleaseNoPage –> setHotPage
后者画风是这样的,实在是太简单了。tls_set_direct 就更简单了。
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
key 是一个宏来的,AUTORELEASE_POOL_KEY 就是用来存放 autoreleasepool 指针的 key。RETURN_DISPOSITION_KEY 就是存下面说的优化的那个 key
# define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
# if SUPPORT_RETURN_AUTORELEASE
# define RETURN_DISPOSITION_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY4)
# endif
好了,这是 autorelease 跟 tls 的瓜葛一。
引用 sunny 的一段话
在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。
这是 autorelease 跟 tls 的瓜葛二。
callerAcceptsFastAutorelease 的简单分析
我们看下什么情况下会支持 FastAutorelease,结论很奇葩除了 Win32 都行。于是说苹果本来打算把 objc 支持 Win32?不是很懂。
// Define SUPPORT_RETURN_AUTORELEASE to optimize autoreleased return values
#if TARGET_OS_WIN32
# define SUPPORT_RETURN_AUTORELEASE 0
#else
# define SUPPORT_RETURN_AUTORELEASE 1
#endif
在源码中搜索 callerAcceptsFastAutorelease 发现这个东西在比较新的 objc 源码中改名字了,这个函数的功能就是检测目标指令,看能否开启优化
callerAcceptsFastAutorelease –> callerAcceptsOptimizedReturn
除了名字改了,实现也改了一点点。原来在 i386 的模拟器上直接返回的是 false,新的不是了。i386 就是 32 位的英特尔架构,x86_64 就是 64 位的 x86 架构。
看两段代码吧,分别是 arm32 和 arm64 的
static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// if the low bit is set, we're returning to thumb mode
if ((uintptr_t)ra & 1) {
// 3f 46 mov r7, r7
// we mask off the low bit via subtraction
// 16-bit instructions are well-aligned
if (*(uint16_t *)((uint8_t *)ra - 1) == 0x463f) {
return true;
}
} else {
// 07 70 a0 e1 mov r7, r7
// 32-bit instructions may be only 16-bit aligned
if (*(unaligned_uint32_t *)ra == 0xe1a07007) {
return true;
}
}
return false;
}
这里指令集分了 thumb 指令集和 arm 指令集,分别对应 16-bit 和 32-bit。thumb 指令的特征是最低位有效。
static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// fd 03 1d aa mov fp, fp
// arm64 instructions are well-aligned
if (*(uint32_t *)ra == 0xaa1d03fd) {
return true;
}
return false;
}
arm64 的就很简单,直接比较指令是不是 0xaa1d03fd。也就是 mov fp, fp 或者 mov x29, x29。
Mike Ash 在他的文章Friday Q&A 2014-05-09: When an Autorelease Isn’t 中给了一个例子
// callee
obj = [[SomeClass alloc] init];
[obj setup];
return [obj autorelease];
// caller
obj = [[self method] retain];
[obj doStuff];
[obj release];
并解释说:
There is some extremely fancy and mind-bending code in the Objective-C runtime’s implementation of autorelease. Before actually sending an autorelease message, it first inspects the caller’s code. If it sees that the caller is going to immediately call objc_retainAutoreleasedReturnValue, it completely skips the message send. It doesn’t actually do an autorelease at all. Instead, it just stashes the object in a known location, which signals that it hasn’t sent autorelease at all.
如果调用方接下里是立刻调用 objc_retainAutoreleasedReturnValue 就会走优化流程。
被调的函数如何知道调用方的信息呢?
__builtin_return_address
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition)
{
assert(getReturnDisposition() == ReturnAtPlus0);
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}
return false;
}
在 prepareOptimizedReturn 函数里调用的 callerAcceptsOptimizedReturn,并用 __builtin_return_address 取当前函数的返回地址。
四个用来做优化的函数
There are two optimized callees:
objc_autoreleaseReturnValue
result is currently +1. The unoptimized path autoreleases it.
objc_retainAutoreleaseReturnValue
result is currently +0. The unoptimized path retains and autoreleases it.
There are two optimized callers:
objc_retainAutoreleasedReturnValue
caller wants the value at +1. The unoptimized path retains it.
objc_unsafeClaimAutoreleasedReturnValue
caller wants the value at +0 unsafely. The unoptimized path does nothing.
他们的声明是
// Prepare a value at +1 for return through a +0 autoreleasing convention.
OBJC_EXPORT id _Nullable
objc_autoreleaseReturnValue(id _Nullable obj);
// Prepare a value at +0 for return through a +0 autoreleasing convention.
OBJC_EXPORT id _Nullable
objc_retainAutoreleaseReturnValue(id _Nullable obj);
// Accept a value returned through a +0 autoreleasing convention for use at +1.
OBJC_EXPORT id _Nullable
objc_retainAutoreleasedReturnValue(id _Nullable obj);
// Accept a value returned through a +0 autoreleasing convention for use at +0.
OBJC_EXPORT id _Nullable
objc_unsafeClaimAutoreleasedReturnValue(id _Nullable obj);
分别用来针对引用计数为 1/0 的对象做优化。