switch執行效率

馬萬旻發表於2018-04-27

任何錯誤,歡迎指正~~~

下面是系統和一些七七八八的東西,一定要用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語句處放一個斷點,執行後結果如下圖(截圖不全):

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指令的執行(對這個有疑惑的可以參考這篇文章 ),偷一張圖~
    switch執行效率

註釋的很清楚啦,我們看下邏輯:

  • 判斷是否滿足第一個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>
    switch執行效率
    我們可以看到,x8暫存器的地址下,存了幾個很有意思的資料: 0xffffff94, 0xffffffa8, 0xffffffbc, 0xffffffd0 而後我們經過ldrsw指令取到了特定的值,存在x10暫存器
  • 斷點到<+64>
    switch執行效率
    將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

相關文章