2012-05-15 186 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

相关问题