2012-05-15 194 views
11

我有兩個文件:爲什麼打電話時jmps就足夠了?

#include <stdio.h> 

static inline void print0() { printf("Zero"); } 
static inline void print1() { printf("One"); } 
static inline void print2() { printf("Two"); } 
static inline void print3() { printf("Three"); } 
static inline void print4() { printf("Four"); } 

int main() 
{ 
    unsigned int input; 
    scanf("%u", &input); 

    switch (input) 
    { 
     case 0: print0(); break; 
     case 1: print1(); break; 
     case 2: print2(); break; 
     case 3: print3(); break; 
     case 4: print4(); break; 
    } 
    return 0; 
} 

#include <stdio.h> 

static inline void print0() { printf("Zero"); } 
static inline void print1() { printf("One"); } 
static inline void print2() { printf("Two"); } 
static inline void print3() { printf("Three"); } 
static inline void print4() { printf("Four"); } 

int main() 
{ 
    unsigned int input; 
    scanf("%u", &input); 

    static void (*jt[])() = { print0, print1, print2, print3, print4 }; 
    jt[input](); 
    return 0; 
} 

我希望它們能夠被編譯到幾乎相同的彙編代碼。在這兩種情況下都會生成跳轉表,但first file中的呼叫由jmp表示,而second one中的呼叫由call表示。爲什麼編譯器不優化call?可以提示gcc我想看jmp s而不是call s?

編譯gcc -Wall -Winline -O3 -S -masm=intel,GCC版本4.6.2。 GCC 4.8.0生成的代碼稍少,但問題仍然存在。

UPD:定義jtconst void (* const jt[])() = { print0, print1, print2, print3, print4 };,使功能static const inline沒有幫助:http://ideone.com/97SU0

+0

是否有性能差異?而且我不相信第二種情況下的呼叫是內聯的。你可以發佈程序集嗎? – Mysticial

+3

也許是因爲在後一種情況下,函數間接調用?如果將jt [] 5個常量指針的常量數組設置爲函數,會發生什麼? –

+0

@Alex - 你打敗了我!非const指針數組可以在運行時修改。 –

回答

2

第一種情況(通過switch())爲我創建以下(x86_64的Linux的/ GCC 4.4):

400570:  ff 24 c5 b8 06 40 00 jmpq *0x4006b8(,%rax,8) 
[ ... ] 
    400580:  31 c0     xor %eax,%eax 
    400582:  e8 e1 fe ff ff   callq 400468 <[email protected]> 
    400587:  31 c0     xor %eax,%eax 
    400589:  48 83 c4 08    add $0x8,%rsp 
    40058d:  c3      retq 
    40058e:  bf a4 06 40 00   mov $0x4006a4,%edi 
    400593:  eb eb     jmp 400580 <main+0x30> 
    400595:  bf a9 06 40 00   mov $0x4006a9,%edi 
    40059a:  eb e4     jmp 400580 <main+0x30> 
    40059c:  bf ad 06 40 00   mov $0x4006ad,%edi 
    4005a1:  eb dd     jmp 400580 <main+0x30> 
    4005a3:  bf b1 06 40 00   mov $0x4006b1,%edi 
    4005a8:  eb d6     jmp 400580 <main+0x30> 
[ ... ] 
Contents of section .rodata: 
[ ... ] 
4006b8 8e054000 p ... ] 

注意.rodata內容@4006b8打印網絡字節順序(無論出於何種原因...) ,值爲40058e,它在上面的main之內 - 其中arg初始值設定程序/ jmp塊啓動。所有在那裏使用8個字節的mov/jmp對,因此間接使用(,%rax,8)。在這種情況下,該序列因此:

jmp <to location that sets arg for printf()> 
... 
jmp <back to common location for the printf() invocation> 
... 
call <printf> 
... 
retq 

這意味着編譯器實際上已經優化了static調用點 - 而是合併他們都到一個單一的,內聯printf()電話。這裏使用的表格是jmp ...(,%rax,8)指令,表格中包含程序代碼內的

第二個(用明確創建的表),併爲我以下:

0000000000400550 <print0>: 
[ ... ] 
0000000000400560 <print1>: 
[ ... ] 
0000000000400570 <print2>: 
[ ... ] 
0000000000400580 <print3>: 
[ ... ] 
0000000000400590 <print4>: 
[ ... ] 
00000000004005a0 <main>: 
    4005a0:  48 83 ec 08    sub $0x8,%rsp 
    4005a4:  bf d4 06 40 00   mov $0x4006d4,%edi 
    4005a9:  31 c0     xor %eax,%eax 
    4005ab:  48 8d 74 24 04   lea 0x4(%rsp),%rsi 
    4005b0:  e8 c3 fe ff ff   callq 400478 <[email protected]> 
    4005b5:  8b 54 24 04    mov 0x4(%rsp),%edx 
    4005b9:  31 c0     xor %eax,%eax 
    4005bb:  ff 14 d5 60 0a 50 00 callq *0x500a60(,%rdx,8) 
    4005c2:  31 c0     xor %eax,%eax 
    4005c4:  48 83 c4 08    add $0x8,%rsp 
    4005c8:  c3      retq 
[ ... ] 
500a60 50054000 00000000 60054000 00000000 [email protected]`[email protected] 
500a70 70054000 00000000 80054000 00000000 [email protected]@..... 
500a80 90054000 00000000     [email protected] 

同樣要注意倒字節順序objdump的打印數據部分 - 如果你把這些你身邊得到功能地址爲print[0-4]()

編譯器通過間接call調用目標 - 即表的使用是直接在call指令,並且該表已經_explicitly被創建爲數據。

編輯:
如果改變這樣的源:

#include <stdio.h> 

static inline void print0() { printf("Zero"); } 
static inline void print1() { printf("One"); } 
static inline void print2() { printf("Two"); } 
static inline void print3() { printf("Three"); } 
static inline void print4() { printf("Four"); } 

void main(int argc, char **argv) 
{ 
    static void (*jt[])() = { print0, print1, print2, print3, print4 }; 
    return jt[argc](); 
}

創建大會main()變爲:

0000000000400550 <main>: 
    400550:  48 63 ff    movslq %edi,%rdi 
    400553:  31 c0     xor %eax,%eax 
    400555:  4c 8b 1c fd e0 09 50 mov 0x5009e0(,%rdi,8),%r11 
    40055c:  00 
    40055d:  41 ff e3    jmpq *%r11d 

這看起來更像是你想要的嗎?

原因是你需要「無堆棧」funcs才能做到這一點 - tail-recursion(通過jmp而不是ret從函數返回)只有在你已經完成所有堆棧清理的情況下才有可能,或者不必做任何事情,因爲你沒有任何東西需要清理。編譯器可以(但不需要)在最後一次函數調用之前選擇清除(在這種情況下,最後一次調用可以由jmp進行),但只有當您返回從該函數獲得的值時,或者如果您「return void」。並且,如上所述,如果你實際上使用使用堆棧(就像你的例子爲input變量所做的那樣),沒有任何東西可以讓編譯器強制以這種方式解除尾遞歸結果。

EDIT2:

拆卸的第一個例子,用同樣的變化(中input並迫使void mainargc代替 - 沒有標準的一致性意見,請這是一個演示),結果如下組件:

0000000000400500 <main>: 
    400500:  83 ff 04    cmp $0x4,%edi 
    400503:  77 0b     ja  400510 <main+0x10> 
    400505:  89 f8     mov %edi,%eax 
    400507:  ff 24 c5 58 06 40 00 jmpq *0x400658(,%rax,8) 
    40050e:  66      data16 
    40050f:  90      nop 
    400510:  f3 c3     repz retq 
    400512:  bf 3c 06 40 00   mov $0x40063c,%edi 
    400517:  31 c0     xor %eax,%eax 
    400519:  e9 0a ff ff ff   jmpq 400428 <[email protected]> 
    40051e:  bf 41 06 40 00   mov $0x400641,%edi 
    400523:  31 c0     xor %eax,%eax 
    400525:  e9 fe fe ff ff   jmpq 400428 <[email protected]> 
    40052a:  bf 46 06 40 00   mov $0x400646,%edi 
    40052f:  31 c0     xor %eax,%eax 
    400531:  e9 f2 fe ff ff   jmpq 400428 <[email protected]> 
    400536:  bf 4a 06 40 00   mov $0x40064a,%edi 
    40053b:  31 c0     xor %eax,%eax 
    40053d:  e9 e6 fe ff ff   jmpq 400428 <[email protected]> 
    400542:  bf 4e 06 40 00   mov $0x40064e,%edi 
    400547:  31 c0     xor %eax,%eax 
    400549:  e9 da fe ff ff   jmpq 400428 <[email protected]> 
    40054e:  90      nop 
    40054f:  90      nop 

這是一個糟糕的方式(不 jmp而不是一個),但在另一個(更好,因爲它消除了static功能和內聯代碼)。在優化方面,編譯器幾乎完成了同樣的事情。

+0

謝謝!我得到了非常接近的結果 - 到裝配輸出的鏈接在OP後。然而,問題是 - 爲什麼編譯器不會在第二種情況下將'call'優化爲兩個'jmp's(如第一種情況)?這會消除堆棧操作,這可能比兩個'jmp'慢。 – Joulukuusi

+0

我完全不明白你的意思;在這兩種情況下,都是通過'call'調用實際的函數。即這裏創建的'main()'不是_tail遞歸_(如果它使用'jmp print ...'而不是'call print ...; retq')。但其原因是'main()'做了一個'return 0'的事實 - 因此它不能是tail-recursive_。 –

+0

但是關於堆棧使用情況,還有一個事實就是考慮爲'scanf()'提供'&input'(一個局部變量的地址)強制堆棧分配。反過來,即使你使返回類型爲「void」,也會阻止編譯器進行函數尾遞歸。看我的編輯。 –

3

我的猜測是,這個優化指的是事實,你的switch後立即有return語句來完成:優化器意識到它可以搭載嵌入到您的print0 .. print4函數中的回報,並將call減少爲jmp; ret CPU中選中的printN作爲從main返回的內容。

嘗試在之後插入一些代碼以查看編譯器是否將jmp替換爲call

#include <stdio.h> 

static inline void print0() { printf("Zero"); } 
static inline void print1() { printf("One"); } 
static inline void print2() { printf("Two"); } 
static inline void print3() { printf("Three"); } 
static inline void print4() { printf("Four"); } 

int main() 
{ 
    unsigned int input; 
    scanf("%u", &input); 

    switch (input) 
    { 
     case 0: print0(); break; 
     case 1: print1(); break; 
     case 2: print2(); break; 
     case 3: print3(); break; 
     case 4: print4(); break; 
    } 
    /* Inserting this line should force the compiler to use call */ 
    printf("\nDone"); 
    return 0; 
} 

編輯: 您對ideone代碼有不同的原因jmp:它是這樣一個等價的:

static const char* LC0 ="Zero"; 
static const char* LC1 ="One"; 
static const char* LC2 ="Two"; 
static const char* LC3 ="Three"; 
static const char* LC4 ="Four"; 

int main() 
{ 
    unsigned int input; 
    scanf("%u", &input); 

    switch (input) 
    { 
     case 0: printf(LC0); break; 
     case 1: printf(LC1); break; 
     case 2: printf(LC2); break; 
     case 3: printf(LC3); break; 
     case 4: printf(LC4); break; 
    } 
    printf("\nDone"); 
    return 0; 
} 
+0

謝謝!儘管如此,即使在這種情況下,編譯器也會輸出'jmp':http://ideone.com/FBHuZ – Joulukuusi

+0

@Joulukuusi請參閱編輯。 – dasblinkenlight

+0

哦,沒錯。我稍微修改了第一個文件:http://ideone.com/GJPQi儘管如此,輸出中還是有'jmp':http://ideone.com/F9AMo – Joulukuusi

5

因爲函數指針數組是可變的。編譯器已經決定不能假定指針不會被改變。你可能會發現C++的程序集是不同的,並且/或者使用jt const。

+0

我不特別買這個說法。證明數組永遠不會改變是微不足道的。它被聲明爲'main'的局部,並且永遠不會在'main()'中被修改 - 它的地址也不會被使用。不管編譯器是否執行這個數據依賴性分析,都是一個不同的故事。 – Mysticial

+0

這樣做,並使'jt'' const'不會改變任何東西。 –

+0

謝謝!不幸的是,將'jt'的聲明更改爲'const void(* const jt [])()'沒有幫助。 – Joulukuusi

8

編譯作家有很多的工作要做。顯然,他們優先考慮具有最大和最快回報的工作。

開關語句在所有類型的代碼中都很常見,所以對它們執行的任何優化都會影響很多程序。

此代碼

jt[input](); 

是少了很多常見,因此很多更長的編譯器設計師TODO名單上了。也許他們還沒有發現嘗試優化它是值得的。這會贏得他們任何已知的基準嗎?或者改進一些廣泛使用的代碼庫?

1

你分析了不同的代碼嗎?我認爲可能會提出一個說法,即間接呼叫已優化。以下分析是用GCC 4完成的。6.1針對x64平臺(MinGW)。

如果你看的時候jt[input]()使用會發生什麼,在下面的代碼序列的調用會導致執行:

  • 到的printX()功能之一間接調用
  • printX()功能設置參數爲printf(),然後
  • 跳到printf()
  • printf()調用將直接返回`間接調用的網站。

共有3個分支。

當您使用switch語句發生的事情是:

  • 間接跳轉到一點的自定義代碼爲每一種情況下(內聯printX()電話)
  • 的「辦案人員」加載相應的論據在printf()呼叫
  • 調用printf()
  • printf()通話將返回到「辦案人員」,這
  • 跳轉到交換機的出口點(其中退出代碼內聯除了一種情況下的處理程序 - 其它情況下跳轉到那裏)

對於總的4個分支(在一般情況下)。

在這兩種情況下,你必須: - 間接分支(一個是電話,在對方跳) - 一個分支到printf()(一這是一個跳躍,在對方通話) - 一個分支返回呼叫站點

但是,當使用switch語句時,有一個額外的分支到達交換機的「結尾」(在大多數情況下)。

現在,如果你實際上分析了事物,處理器可能會比間接調用更快地處理間接跳轉,但我猜測即使是這種情況,基於開關的代碼中使用的額外分支也會通過函數指針仍然推動比例尺的調用。


對於那些有興趣,這裏是用jk[input]();(這兩個例子中使用GCC編譯的MinGW 4.6.1針對64位,使用的選項是-Wall -Winline -O3 -S -masm=intel)生成彙編:

print0: 
    .seh_endprologue 
    lea rcx, .LC4[rip] 
    jmp printf 
    .seh_endproc 

// similar code is generated for each printX() function 
// ... 

main: 
    sub rsp, 56 
    .seh_stackalloc 56 
    .seh_endprologue 
    call __main 
    lea rdx, 44[rsp] 
    lea rcx, .LC5[rip] 
    call scanf 
    mov edx, DWORD PTR 44[rsp] 
    lea rax, jt.2423[rip] 
    call [QWORD PTR [rax+rdx*8]] 
    xor eax, eax 
    add rsp, 56 
    ret 

這裏的代碼生成基於交換機的實現:

main: 
    sub rsp, 56 
    .seh_stackalloc 56 
    .seh_endprologue 
    call __main 
    lea rdx, 44[rsp] 
    lea rcx, .LC0[rip] 
    call scanf 
    cmp DWORD PTR 44[rsp], 4 
    ja .L2 
    mov edx, DWORD PTR 44[rsp] 
    lea rax, .L8[rip] 
    movsx rdx, DWORD PTR [rax+rdx*4] 
    add rax, rdx 
    jmp rax 
    .section .rdata,"dr" 
    .align 4 
.L8: 
    .long .L3-.L8 
    .long .L4-.L8 
    .long .L5-.L8 
    .long .L6-.L8 
    .long .L7-.L8 
    .section .text.startup,"x" 
.L7: 
    lea rcx, .LC5[rip] 
    call printf 
    .p2align 4,,10 


.L2: 
    xor eax, eax 
    add rsp, 56 
    ret 

.L6: 
    lea rcx, .LC4[rip] 
    call printf 
    jmp .L2 

    // all the other cases are essentially the same as the one above (.L6) 
    // where they jump to .L2 to exit instead of simply falling through to it 
    // like .L7 does 
+0

謝謝!我想描述純粹的說明,但我找不到一種方法來實現這一點。我的程序集輸出與你的稍有不同 - 當使用'jt [input]()'時,會產生'call _printf'(而不是你的'jmp printf')。另外,在調用'printf'之前和之後有一些堆棧對齊。這很重要嗎? – Joulukuusi

+0

@Joulukuusi:我認爲您在生成64位x64代碼時生成32位x86代碼。它看起來像x86代碼生成略優於間接調用大小的x64代碼生成。但是,我仍然不確定它最終會比switch語句更不理想(但也許)。我認爲堆棧調整是由於調用爭用要求(?)而完成的。這些調整可以在'switch'中優化,因爲它可以內聯間接調用,因爲通過特定的指針進行單獨的間接調用。 –

+0

我認爲原則上,編譯器可以將'jt [input]()'調用'擴展'爲五個獨立的直接調用,並以switch語句結束相同的代碼,但正如Bo Persson所述,該場景可能不足以引起編譯器維護者的注意。請注意,調用執行堆棧對齊的函數的x86代碼仍然具有與switch語句生成的「優化」代碼類似的分支數量(以我的計數方式爲4個分支)。所以它仍然可能與x86上的'switch'完全相同。 –

1

確實爲後一功能的代碼做間接call之間沒有任何東西和以後的ret?如果間接調用的地址計算使用後一個函數需要保留的值(意味着它必須在計算之前保存該值並在一段時間後恢復),那麼我不會感到驚訝。雖然在間接調用之前移動寄存器恢復代碼是可能的,但編譯器只能在已被編程識別爲合法機會的情況下執行此類代碼移動。

另外,雖然我不認爲它很重要,但我建議例程不應該是inline,因爲編譯器將無法以這種方式執行它們。

+0

謝謝!是的,它使用'eax'進行地址計算 - '調用[DWORD PTR _jt.1677 [0 + eax * 4]]'。在此之後(即被調用的函數返回之後)跟隨'xor eax,eax','leave'和'ret'。找不到爲什麼必須保留'eax'。順便說一句,我在這個問題中包含了組裝輸出的鏈接。關於'內聯'的建議 - 你能解釋一下嗎? – Joulukuusi

+0

@Joulukuusi:返回值在eax中。由於編譯器無法推斷被調用函數返回時eax中的值,因此它必須將eax加載到零。如果要從'void'函數進行間接調用,或者調用類型爲'int'的函數並返回該函數返回的值,則可以消除'xor',也許可以允許使用間接'jmp '而不是'呼叫'。 – supercat

相關問題