0%

当编译优化遇上AutoRelease优化

最近在复习自动释放池,在ARC下runtime有一个优化objc_autoreleaseReturnValueobjc_retainAutoreleaseReturnValue,但是优化遇上编译器优化,似乎有点问题…不知道算不算苹果自己给自己挖的坑。

背景

现讲述一下大致的背景:(如果很了解autorelease及其优化原理可以直接跳过这一节)
MRC:

在MRC下,本着谁分配谁释放的原则,在方法返回方法内部创建的某个对象的时候,其实是需要释放掉对象的(因为当前方法栈已经结束)。但是矛盾的是如果释放了,那这个方法如果返回这个对象给调用者,调用者又如何取到?

所以,在返回对象的时候,不释放对象,而是把对象加入到 自动释放池 内(其实这里可以理解为,把对象的所有权交给了自动释放池),此时自动释放池持有了这个对象(引用计数为1)。调用者依旧可以拿到这个对象地址,对象依旧有效。

当然调用者自己是否需要持有它,那是调用者的逻辑了,持有与释放相互匹配。

ARC:

在ARC下,需要依旧保持MRC下的“谁分配谁释放”的原则。但是ARC下编译器不允许调用autorelease方法。编译器会给代码的返回值后自动插入autorelease。

runtime优化:

在MRC下,如果调用者和被调用者都是模块内自己调用,那么其实可以忽略掉自动释放池这一个步骤。换句话说,如果可以保证调用者有变量在接收返回值,那么被调用者可以不用加入自动释放池,直接把对象的所有权转移给函数接受者。

ps:当然这样会有风险,前提是模块内调用,如果是模块间调用,没有变量接收返回值的话,那么就会造成内存泄漏了。

那么在ARC下如何优化呢?

runtime结合编译器自动实现了这个过程。其实runtime的优化思路和上面MRC环境下开发者的优化思路是一样的,只不过把这个过程转换为了代码。

在被调用方使用__builtin_return_address(int depth)这个函数判断调用方的接受者是否使用了这个优化函数objc_retainAutoreleaseReturnValue(有两种情况不使用,一种是MRC,一种是没有接受者),如果使用了优化那么被调用方objc_autoreleaseReturnValue就会把某个标记位flag设置到TLS。

如果调用方不使用优化,那么就是走自动释放池的逻辑;如果调用方使用了优化函数objc_retainAutoreleaseReturnValue,那么先去TLS看有没有这个标记位(防止被调用方没有优化),如果有,直接使用返回的值,无需再调用retain了。

简单总结:

如果双方都使用优化,那么就直接用返回的对象,被调用方不释放,调用方不持有。实际就是对象拥有者直接转移。

调用者通过TLS的flag判断被调用者是否使用了优化。

被调用者通过__builtin_return_address(int depth)这个函数判断调用者是否使用优化。

问题

在理解的过程中,我一直以为这个在ARC下的实验结果应该没有悬念的…但现实却给了我狠狠的一巴掌。

我在Xcode里新建了一个 Mac 应用程序的Demo,ARC,方法返回的值始终在自动释放池内。

下面是测试代码,测试代码来自网络..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
///////////////////////////////////////////////////////////////////////////////////////////// 1.
id obj0;
{
id obj1 = [NSMutableArray array];
obj0 = obj1;
_objc_autoreleasePoolPrint();
}
NSLog(@"obj0-%@", obj0);

///////////////////////////////////////////////////////////////////////////////////////////// 2.
id __weak obj2;
{
id obj3 = [NSMutableArray array];
obj2 = obj3;
_objc_autoreleasePoolPrint();
}
NSLog(@"obj2-%@", obj2);

///////////////////////////////////////////////////////////////////////////////////////////// 3.
id __weak obj4;
@autoreleasepool {
id obj5 = [NSMutableArray array];
obj4 = obj5;
_objc_autoreleasePoolPrint();
}
NSLog(@"obj4-%@", obj4);
return 0;
}

上面这3段代码,区别在于后两者插入了一个__weak指针的赋值,但是实际结果却是三个数组都在自动释放池内。

于是我找到了这篇 文章….

然后我又在真机上调试了一下,确实,第一个没有进自动释放池,后两个都进了。

文章作者最后给的解释比较模糊(后面两个没有调用retain),于是我只能自己再找原因。

解决

Runtime其实已经有源码可寻,。关键在于调用者的那个优化函数的指令地址。

那么就看看汇编代码,到底有什么区别…
下面是ARM(真机)的汇编代码:

在runtime的实现里会直接比较调用者下一个函数在代码内的地址与objc_retainAutoreleasedReturnValue的函数地址。很明显,第一个之所以不在池内是因为后面紧跟着objc_retainAutoreleasedReturnValue,而第二个就不是。

第二个和第一个的区别只有一个weak指针的赋值,于是我把赋值也去掉了。

1
2
3
4
5
6
id __weak obj2;
{
id obj3 = [NSMutableArray array];
_objc_autoreleasePoolPrint();
}
NSLog(@"obj2-%@", obj2);

结果依旧如此….

??????

我第一反应就想到了编译器的优化。

默认情况下,Debug的优化级别为None,Release的是Fastest,Smallest[-Os]。我把Debug改为和Release一样…

结果……就和预期一样,三个都不会在自动释放池内,也就是说在真机上运行是符合预期的。

编译器不优化会导致 objc_retainAutoreleasedReturnValue和返回函数之间被插入代码,优化了反而不会被插入…

对编译器不了解,这里没有任何发言权,本文也只是一些实验和猜测…

另外x86的iOS模拟器和mac程序,调整优化level也只能部分达到预期…