switch执行效率

1,373 阅读11分钟

任何错误,欢迎指正~~~

下面是系统和一些七七八八的东西,一定要用64位CPU的真机

C语言
arm64
Xcode9.3
iOS11.3

准备

  • 因为我们需要从汇编的角度去研究这个问题,首先打开Xcode的汇编调试,依次选择 Debug->DebugWorkflow->Always Show Disassembly
  • 注意要在debug模式下调试,release模式下编译器优化后你可能只能看到两行汇编了

三个case分支的switch

首先我们写一个拥有三个case的switch;

void func(int a) {
    switch (a) {
        case 1:
            printf("1");
            break;
        case 2:
            printf("2");
            break;
        case 3:
            printf("3");
            break;
        default:
            printf("default");
            break;
    }
}

int main(int argc, char * argv[]) {
    func(2);
    return 0;
}

在func函数switch语句处放一个断点,执行后结果如下图(截图不全):

把代码复制下来方便注释:

    0x100d56838 <+0>:   sub    sp, sp, #0x40        
    0x100d5683c <+4>:   stp    x29, x30, [sp, #0x30]
    0x100d56840 <+8>:   add    x29, sp, #0x30          
    0x100d56844 <+12>:  stur   w0, [x29, #-0x4]
    
    ; 参数=0x1
->  0x100d56848 <+16>:  ldur   w0, [x29, #-0x4]
    0x100d5684c <+20>:  mov    x8, x0 
    0x100d56850 <+24>:  subs   w0, w0, #0x1   ;w0-1        
    0x100d56854 <+28>:  stur   w8, [x29, #-0x8]
    0x100d56858 <+32>:  stur   w0, [x29, #-0xc]
    0x100d5685c <+36>:  b.eq   0x100d5688c              ; <+24>行的执行结果,参数是否等于1,等于则跳转到 0x100d5688c
    0x100d56860 <+40>:  b      0x100d56864               
    
    ; 参数=0x2
    0x100d56864 <+44>:  ldur   w8, [x29, #-0x8]
    0x100d56868 <+48>:  subs   w9, w8, #0x2               
    0x100d5686c <+52>:  stur   w9, [x29, #-0x10]
    0x100d56870 <+56>:  b.eq   0x100d568a0              ; <+48>行的执行结果,参数是否等于2,等于则跳转到 0x100d568a0
    0x100d56874 <+60>:  b      0x100d56878 
    
    ; 参数=0x3
    0x100d56878 <+64>:  ldur   w8, [x29, #-0x8]
    0x100d5687c <+68>:  subs   w9, w8, #0x3              
    0x100d56880 <+72>:  stur   w9, [x29, #-0x14]
    0x100d56884 <+76>:  b.eq   0x100d568b4              ;
    <+68>行的执行结果,参数是否等于3,等于则跳转到 0x100d568b4
    0x100d56888 <+80>:  b      0x100d568c8              ; 前三个分支均不匹配,跳转到default         
    
    ;打印1
    0x100d5688c <+84>:  adrp   x0, 1
    0x100d56890 <+88>:  add    x0, x0, #0xf24            
    0x100d56894 <+92>:  bl     0x100d56bf4               ; symbol stub for: printf
    0x100d56898 <+96>:  str    w0, [sp, #0x18]
    0x100d5689c <+100>: b      0x100d568d8               
    
    ;打印2
    0x100d568a0 <+104>: adrp   x0, 1
    0x100d568a4 <+108>: add    x0, x0, #0xf26            
    0x100d568a8 <+112>: bl     0x100d56bf4               ; symbol stub for: printf
    0x100d568ac <+116>: str    w0, [sp, #0x14]
    0x100d568b0 <+120>: b      0x100d568d8               
    
    ;打印3
    0x100d568b4 <+124>: adrp   x0, 1
    0x100d568b8 <+128>: add    x0, x0, #0xf28            
    0x100d568bc <+132>: bl     0x100d56bf4               ; symbol stub for: printf
    0x100d568c0 <+136>: str    w0, [sp, #0x10]
    0x100d568c4 <+140>: b      0x100d568d8               
    
    ;打印default
    0x100d568c8 <+144>: adrp   x0, 1
    0x100d568cc <+148>: add    x0, x0, #0xf2a            
    0x100d568d0 <+152>: bl     0x100d56bf4               ; symbol stub for: printf
    0x100d568d4 <+156>: str    w0, [sp, #0xc]
    
    ;函数结束
    0x100d568d8 <+160>: ldp    x29, x30, [sp, #0x30]
    0x100d568dc <+164>: add    sp, sp, #0x40            
    0x100d568e0 <+168>: ret    

汇编代码顺序执行,关键代码添加了注释,有几个关键的寄存器和指令:

  • x0(w0)为我们传递的参数,也就是1;
  • subs指令会修改cpsr寄存器,这个寄存器会影响到B指令的执行(对这个有疑惑的可以参考这篇文章 ),偷一张图~

注释的很清楚啦,我们看下逻辑:

  • 判断是否满足第一个case(等于1),满足跳转打印,不满足继续执行;
  • 判断是否满足第二个case(等于2),满足跳转打印,不满足继续执行;
  • 判断是否满足第三个case(等于3),满足跳转打印,不满足执行default;

这不就是if判断嘛!别急。。。继续往下看


多于三个分支的switch

这是switch:

void func(int a) {
    switch (a) {
        case 1:
            printf("1");
            break;
        case 2:
            printf("2");
            break;
        case 3:
            printf("3");
            break;
        case 4:
            printf("4");
            break;
        default:
            printf("default");
            break;
    }
}

int main(int argc, char * argv[]) {
    func(2);
    return 0;
}

汇编代码如下,不截图啦

    0x104cae820 <+0>:   sub    sp, sp, #0x40             
    0x104cae824 <+4>:   stp    x29, x30, [sp, #0x30]
    0x104cae828 <+8>:   add    x29, sp, #0x30            
    0x104cae82c <+12>:  stur   w0, [x29, #-0x4]
    
    ;参数减1,即第一个case的值
->  0x104cae830 <+16>:  ldur   w0, [x29, #-0x4]
    0x104cae834 <+20>:  subs   w0, w0, #0x1              ; =0x1 
    ;参数减3,共减4,即最后一个case分支的值
    0x104cae838 <+24>:  mov    x8, x0
    0x104cae83c <+28>:  subs   w0, w0, #0x3              ; =0x3
    
    ;注意这里,我们把减去第一个case的值(注意结果哦)入栈了[x29, #-0x10]
    0x104cae840 <+32>:  stur   x8, [x29, #-0x10]
    0x104cae844 <+36>:  stur   w0, [x29, #-0x14]
    
    ;参数总共减去了4,也就是看他是否满足最大值4,不满足则跳转default
    0x104cae848 <+40>:  b.hi   0x104cae8b4  
    
    ;这部分计算过后,会取到一个<地址>放到x8寄存器
    0x104cae84c <+44>:  adrp   x8, 0
    0x104cae850 <+48>:  add    x8, x8, #0x8d0            
    
    ;x9寄存器取到刚才入栈的值,也就是参数减去第一个case的值(注意结果哦)
    0x104cae854 <+52>:  ldur   x9, [x29, #-0x10]

    ;以下三条指令会获取到一个地址并跳转,跳转的位置刚好是我们传入的参数对应的case语句(缘分啊~)
    0x104cae858 <+56>:  ldrsw  x10, [x8, x9, lsl #2]    
    ;ldrsw:这个指令的意思是读取内存地址中的2个字(word,即4个字节,32位),并且把高位的符号位作为扩展填充到64位的寄存器中。
    ;这条指令会进行如下操作:
    ;1.将x9寄存器的值左移两位(即乘以4)。
    ;2.将x8寄存器的值与这个值相加。
    ;3.将得到内存地址中的值存到x10寄存器。
    0x104cae85c <+60>:  add    x8, x10, x8 ;将之前取到的地址(x8),与x10中的值相加,取得一个地址
    0x104cae860 <+64>:  br     x8
    
    ;第1个case
    0x104cae864 <+68>:  adrp   x0, 1
    0x104cae868 <+72>:  add    x0, x0, #0xf20            ; =0xf20 
    0x104cae86c <+76>:  bl     0x104caebf0               ; symbol stub for: printf
    0x104cae870 <+80>:  str    w0, [sp, #0x18]
    0x104cae874 <+84>:  b      0x104cae8c4               ; <+164> at main.m:30
    
    ;第2个case
    0x104cae878 <+88>:  adrp   x0, 1
    0x104cae87c <+92>:  add    x0, x0, #0xf22            ; =0xf22 
    0x104cae880 <+96>:  bl     0x104caebf0               ; symbol stub for: printf
    0x104cae884 <+100>: str    w0, [sp, #0x14]
    0x104cae888 <+104>: b      0x104cae8c4               ; <+164> at main.m:30
    
    ;第3个case
    0x104cae88c <+108>: adrp   x0, 1
    0x104cae890 <+112>: add    x0, x0, #0xf24            ; =0xf24 
    0x104cae894 <+116>: bl     0x104caebf0               ; symbol stub for: printf
    0x104cae898 <+120>: str    w0, [sp, #0x10]
    0x104cae89c <+124>: b      0x104cae8c4               ; <+164> at main.m:30
    
    ;第4个case
    0x104cae8a0 <+128>: adrp   x0, 1
    0x104cae8a4 <+132>: add    x0, x0, #0xf26            ; =0xf26 
    0x104cae8a8 <+136>: bl     0x104caebf0               ; symbol stub for: printf
    0x104cae8ac <+140>: str    w0, [sp, #0xc]
    0x104cae8b0 <+144>: b      0x104cae8c4               ; <+164> at main.m:30
    
    ;default
    0x104cae8b4 <+148>: adrp   x0, 1
    0x104cae8b8 <+152>: add    x0, x0, #0xf28            ; =0xf28 
    0x104cae8bc <+156>: bl     0x104caebf0               ; symbol stub for: printf
    
    ;函数结束
    0x104cae8c0 <+160>: str    w0, [sp, #0x8]
    0x104cae8c4 <+164>: ldp    x29, x30, [sp, #0x30]
    0x104cae8c8 <+168>: add    sp, sp, #0x40             ; =0x40 
    0x104cae8cc <+172>: ret    

不一样了对不对ಠ_ಠ,我们分析下这部分汇编:

  • 断点到<+60>
    我们可以看到,x8寄存器的地址下,存了几个很有意思的数据: 0xffffff94, 0xffffffa8, 0xffffffbc, 0xffffffd0 而后我们经过ldrsw指令取到了特定的值,存在x10寄存器
  • 断点到<+64>
    将x10和x8中的值结合,我们得到了一个地址: 0x0000000104cae878
  • 第<+64>行执行后,我们通过br指令跳转到这个地址,这个地址刚好是<+88>行,也就是我们的case2。(奇迹发生了,完美匹配了,我们之前传入的参数就是2)

哎?什么原理呢?
第<+44>行,我们取到了一个地址存到了X8寄存器,这个地址下存的是什么呢?

实际上这是一个表,每四个字节为一段数据,所以我们在ldrsw指令中会左移两位,也就是乘以4。那么这个数据是什么呢?其实,0xffffff94与x8结合后得到的是case1中执行代码的内存地址,0xffffffa8与x8结合后得到的是case2中执行代码的内存地址,以此类推。至于为什么这个表中的存储的是这个值,我觉得这是编译器做的事~。
同样的在<+32>行把减去第一个case的值入栈,之后在<+52>行把这个值存到x9寄存器,此时这个在ldrsw指令中参与操作的x9寄存器的值,正是一个偏移值,这也是为什么我们要减去case1再入栈的原因。(比如我们传入1,减去case1的值之后就是0,也就是偏移0位,我们从表中取到的值就是第一个值,0xffffff94,而后通过这个值与x8寄存器的值计算,得到的就是case1中执行代码的内存地址。)

结论:

  1. switch通过这种巧妙的方式,在内存中创建了一个表,存储一个用于计算获取case执行指令地址的值。
  2. 将参数对应的case偏移位计算出来存到了x9寄存器中。
  3. 将一个用于计算的基址存在了X8寄存器中。
  4. 通过ldrsw指令,取到这个表中偏移x9的值,和x8进行计算,取到对应case指令的地址。
  5. 跳转到对应的case执行。

未完待续....

非连续的switch

乱序的switch