深入分析objc_msgSend尾调用优化

无意间看到了这篇文章 iOS objc_msgSend尾调用优化详解,讲的是在Release模式下,当某函数的最后一项操作是调用另外一个函数,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack),以实现栈帧复用。

栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。所以在每一次函数调用之前,需要保存下执行的环境,以确保正常返回到调用位置。下面是- (void)viewDidLoad方法的汇编代码

可以看出在调用[super viewDidLoad]之前,先申请栈空间,然后保存x29和x30寄存器的值,调用结束后恢复寄存器的值,然后平栈。x29寄存器也就是fp寄存器,保存栈底的地址。x30寄存器也就是lr寄存器,保存返回地址。那栈帧复用又是怎么实现的呢?本着知其然知其所以然的精神,深入研究下。用下文章中的例子

- (NSInteger)func1:(NSInteger)num{
    if (num >= 2000000) {
        return num;
    }
    return [self func1:num + 1];
}

运行打断点,查看汇编代码如下

可以发现,这里面并没有保存和恢复寄存器的操作,也没有申请栈空间。如果不保存,函数执行完会返回哪里呢?这里就要看下,fp和lr寄存器了。

多次点继续执行,会发现这三个寄存器的值是不会变的。也就是说,被调用函数继承了调用函数的执行环境。这就很好理解了,因为是尾调用,编译器认为已经没必要再返回到调用函数了,当前函数执行完直接返回到最终地址就可以了。

执行环境是包含fp和sp的,也就是标识着栈空间的起始,如果栈空间发生变化,那么还会复用栈帧吗?写以下代码

- (NSInteger)func1:(NSInteger)num{
    NSObject *obj = [NSObject new];
    if (num >= 2000000) {
        return num;
    }
    return [self func1:num + 1];
}

继续调试,发现这种情况下,并没有优化。如图

也就是说,尾调用优化需要同时满足栈空间没有变化。

文章中提到尾调用优化的条件有一条为:尾调用函数不需要访问当前栈帧中的变量。(变量可以作为形参,但是不能作为实参)

如果调用函数的参数数量大于寄存器所能传递的数量时,是需要通过栈传递的。这种情况下,是不是也不会优化呢?写以下代码验证

- (NSInteger)func1:(NSInteger)num{
    if (num >= 2000000) {
        return num;
    }
    return [self funcA:1 B:2 C:3 D:4 E:5 F:6 G:7 H:8 J:9];
}

- (int)funcA:(int)a B:(int)b C:(int)c D:(int)d E:(int)e F:(int)f G:(int)g H:(int)h J:(int)j{
    int value = a + b + c + d + e + f + g + h + j;
    if (value >= 200000) {
        return value;
    }
    return [self funcA:a + 1 B:b + 1 C:c + 1 D:d + 1 E:e + 1 F:f + 1 G:g + 1 H:h +1 J:j + 1];
}

切换到汇编下断点在objc_msgSend前,可以发现当参数大于9个的时候,就需要栈传递了

继续运行可以发现,这里funcA并没有复用func1的栈帧,也就是没有尾调用优化。而funcA函数的栈帧被复用了。

通过以上可以看出,当被调用函数的栈空间大于调用函数的栈空间时,因为需要开辟新的空间,fp和sp是需要变化的,这种情况下是不会优化的。当相等的时候,也是可以尾调用优化的。那会有人问,为什么以下情况没有复用栈帧呢?

- (NSInteger)func1:(NSInteger)num{
    NSObject *obj = [NSObject new];
    if (num >= 2000000) {
        return num;
    }
    return [self func1:num + 1];
}

因为对象是涉及到生命周期的,在调用方法之前,对象还没有释放,栈空间还是使用状态,所以被调用函数无法复用调用函数的栈空间。

既然栈空间相等是可以复用的,那么当小于的时候一定也是可以的,这里就不贴代码了。有兴趣的可以自己验证下。

栈帧复用取决于sp、fp和lr寄存器,那么以下情况也是可以的

- (NSInteger)func1:(NSInteger)num{
    if (num >= 2000000) {
        return num;
    }
    num = [self func2:num];
    return num;
}
- (NSInteger)func2:(NSInteger)num{
    return num;
}

毕竟对于编译器来说直接返回函数和返回函数的返回值是一样的,看汇编确实是这样的

那么这种情况也是可以实现栈复用,而如果返回值不单纯是被调用函数的返回值,当被调用函数返回后还需要做一些需要寄存器相关的操作的话,这种情况是需要保存上下文的,所以不能实现尾调用优化。这里就不做测试了。

综上尾调用优化条件如下:

1、调用函数返回值为被调用函数的返回值或者被调用函数本体。

2、被调用函数的栈空间不能大于调用函数所申请的栈空间。