深入分析objc_msgSend尾调用优化

深入分析objc_msgSend尾调用优化
Photo by Martin Shreder / Unsplash

无意间看到了这篇文章 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、被调用函数的栈空间不能大于调用函数所申请的栈空间。

Read more

Category和关联对象

Category和关联对象

Category也就是分类,可以在没有类定义的情况下为现有类添加方法、属性、协议,众所周知是不能增加成员变量的,但是是可以通过关联对象给类增加的。 struct category_t { const char *name; classref_t cls; WrappedPtr instanceMethods; WrappedPtr classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsF

By LEMON
内存管理-AutoreleasePool

内存管理-AutoreleasePool

AutoreleasePool(自动释放池)是Objective-C中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。 @autoreleasepool { ... }可以通过 clang -rewrite-objc 命令转成C++代码如下 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; } 然后可以从objc源码中找到__AtAutoreleasePool的定义 struct __AtAutoreleasePool { __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() { objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; }; 这个结构体先

By LEMON
如何为macOS应用开发插件

如何为macOS应用开发插件

本文章只用于学习和交流,转载请注明出处。 玩过越狱手机的应该都了解,iOS的越狱插件是可以为系统或者应用添加一些特色的功能的。越狱插件的开发,已经有完整的工具链,并且插件的加载运行Substrate框架已经为我们做好。 更改应用的功能实现有两种方法,一种是通过反编译工具更改汇编代码后生成新的可执行文件。另一种是写一个动态库,然后注入到可执行文件。第一种适合简单的逻辑修改,毕竟用汇编实现功能是需要很大的工作量的。 第二种要怎么注入呢?了解Mach-O 二进制文件的应该知道,Mach-O 二进制文件Load Commands中的 LC_LOAD_DYLIB 标头告诉 macOS在执行期间要加载哪些动态库 (dylib)。所以我们只需要在二进制文件中添加一条LC_LOAD_DYLIB就可以。而insert_dylib工具已经为我们实现了添加的功能。所以现在唯一需要考虑的就是插件如何开发。 废话不多说,现在就以mac版的迅雷为例开发一款属于我们自己的插件。首先下载最新版本的迅雷(版本5.0.2)。然后用Xcode新建一个macOS下的Framework,如下 新建完目录结构如下,如果

By LEMON
深入分析弱引用(__weak)

深入分析弱引用(__weak)

__weak 在开发中经常用到,主要是为了解决内存管理中的循环引用。__weak修饰的指针特性是其指向的对象销毁后,会自动置为 nil。 用到__weak基本上分两种情况,一种是先声明再赋值(@property (weak)也是这种),另一种是声明的时候赋值。 NSObject *obj = [NSObject new]; __weak id weak; weak = obj; __weak id weak1 = obj; 新建个项目,在项目中写入以上代码,然后再终端输入以下命令编译成C++源码 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 "替换为写代码的文件" 可得到以下内容,发现__weak 对应的类型属性都为__attribute((objc_ownership(

By LEMON
京ICP备15024336号-4