iOS-从汇编分析switch在编译时的实现

1,075 阅读3分钟

前言

通过switch汇编代码分析,在编码中更高效的使用判断条件,读者需要一些arm64汇编基础。

使用场景

在开发过程中不少有对一个变量的判断跳转不同的分支或者方法的场景例如以下。

截屏2021-04-01 下午3.36.47.png

接下来我们通过运行查看其汇编的实现:

64a8457ce36b4a68b18ff6d50bf80c08~tplv-k3u1fbpfcp-watermark.image.png

可以看出例子中的switch语句有3个case,这三个case经过汇编之后会变成三组cmp b.eq其等价于三个if else判断的汇编代码,那么如果我们把case变成4个会怎么样呢?

截屏2021-04-01 下午4.01.12.png

汇编实现:

截屏2021-04-01 下午4.03.09.png

显然当case为4个的时候汇编发生了很多的变化,下面来分析一下汇编流程:

源码分析

阶段一

第10行, 将源码result和case1中的1相减,第11行将相间结果保存在x9寄存器

阶段二

第12行, ubfx指令将x9高32位清零,

第13行:将x9的值再和0x3比较(这里为什么和0x3比较呢?,3即是case中最大值减去最小值的差值即4-1=3)

第14行:将x9入栈 第15行: b.hi指令比较结果是无符号大于,执行标号,否则不跳转,如果成立跳转到0x100f3de64执行代码,显然0x100f3de64default的实现。

这里涉及到一个数学问题,x9寄存器保存的是result和case1的差值,3是case中最大和最小的差值,将两个差值做无符号比较,如果前者大于了后者那么result的值一定是比case中最大的值要大的,所有会直接到default执行,如果reuslt为负数,在进行无符号比较时显然负数的值0xffffffff是相当大的,所以这里的一次比较就可以将小于case最小值和大于case最大值的result直接定位到default

代码能执行到第16行即说明result的值是在case最大最小值之间, 这时会查找编译期生成的一个switch偏移表来找到偏移值定位到需要执行代码的位置。以下为switch表的内存分布,可以看出每4个字节保存着一个负数。这个表是怎么找到的,在什么位置?

截屏2021-04-01 下午5.03.34.png

接下来来分析:

阶段三

第16和17行:执行完毕x8寄存器保存的值是0x0000000100f3de7c,可以看出该地址就是函数结束位置

第18行:读出栈中的数据(第一阶段x9保存的差值)到x11

第19行:取出x11中的值逻辑左移2位(即乘以4,这里这个4就是switch偏移表中的数据宽度),再加上x8的值,结果即是对应的switch表中的某个数据的首地址,根据当前的案例该地址就是switch表的首地址,然后读地址中的值取得这个偏移值(是个小于等于零的数)保存的x10寄存器.

第20行:x8 + x10 取得对应case的实现代码的地址值保存在x9(当前案例中0x0000000100f3de7c + 负数0xffffffffffffffa8 = 0x0000000100f3de24

第21行:跳转执行(当前案例中0x0000000100f3de24指向的就是case等于1的实现)

可以看出仅仅经过几次运算一次比较一次查找就可以将switch准确定位,用空间换取了频繁的cmp的性能消耗

总结

3个以下的判断使用switch和if是一样的,三个以上使用switch,但是当switch中的case差值较大时,仍有可能会编译cmp b 的组合,这里编译器到底是怎么根据条件判断生成相关代码的还有待考究。 所以当case很多时建议case连续,这样就可以高效的查表一次定位。