objc_msgSend的尾调用优化

最近再重读「编写高质量iOS与OS X代码的52个有效方法」一书,发现第十一条理解objc_msgSend的作用中有提到尾调用优化(tail-call optimization)这个概念

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的”栈帧“(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。

这段话读了很多遍,但还是一知半解,再查阅了相关资料后,在这里做一个记录。

尾调用优化的条件

函数A的最后一个指令是执行一个函数B (B 也可以为A)

示例

1
2
3
4
5
6
7
8
9
- (NSInteger)funcA:(NSInteger)num {
if (num == 0) {
return [self funcA:num];
}
if (num > 0) {
return [self funcB:num];
}
return [self funcC:num];
}

上述代码就是一个尾调用的例子。

1
2
3
4
- (NSInteger)funcA:(NSInteger)num {
NSInteger result = [self funcB:(num)];
return result;
}

上述代码因为返回的是一个值,所以不属于尾调用。

一般函数的调用过程:

函数调用会在内存中申请一块“栈帧”,保存调用的地址和内部变量等信息。如果函数A内部调用函数B,那么在函数A的栈帧上就会加上一个函数B的栈帧
。如果函数B再调用了函数C,那么函数A的栈帧上就会有序加上函数B和函数C的栈帧。如果C运行结束了,返回到函数B,C的栈帧才会消失。

尾调用优化后的调用过程:

当函数A的最后一步仅仅是调用另一个函数B时(或者调用自身函数A),这时,因为函数A的位置信息和内部变量已经不会再用到了,直接把函数A的栈帧交给函数B使用。

尾调用优化的本质就是栈帧的复用。