为什么叫jmps就足够了?

时间:2012-05-15 13:52:00

标签: c gcc assembly x86

我有两个文件:

#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而不是call s?

使用gcc -Wall -Winline -O3 -S -masm=intel编译,GCC版本4.6.2。 GCC 4.8.0产生的代码略少,但问题仍然存在。

UPD :将jt定义为const void (* const jt[])() = { print0, print1, print2, print3, print4 };并使功能static const inline无效:http://ideone.com/97SU0

6 个答案:

答案 0 :(得分:8)

编译器编写者要做很多工作。显然,他们优先考虑具有最大和最快收益的工作。

Switch语句在所有类型的代码中都很常见,因此对它们执行的任何优化都会对许多程序产生影响。

此代码

jt[input](); 

不常见,因此在编译器设计人员的TODO列表上要长得多。也许他们还没有(还)发现尝试优化它的努力值得吗?这会赢得他们任何已知的基准吗?或者改进一些广泛使用的代码库?

答案 1 :(得分:5)

因为函数指针数组是可变的。编译器已决定不能假设指针不会被更改。您可能会发现C ++的程序集不同,和/或make jt const。

答案 2 :(得分:3)

我的猜测是,这种优化与您在return之后立即有switch语句这一事实有关:优化器意识到它可以捎带嵌入{{1}中的返回值} print0函数,并将print4缩减为call; jmp内所选ret点击的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;
}

答案 3 :(得分:2)

第一种情况(通过switch())为我创建了以下内容(Linux x86_64 / 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 <printf@plt>
  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-initializer / jmp块启动。那里的所有mov / jmp对都使用8个字节,因此(,%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 <scanf@plt>
  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  P.@.....`.@.....
 500a70 70054000 00000000 80054000 00000000  p.@.......@.....
 500a80 90054000 00000000                    ..@.....

再次注意颠倒的字节顺序,因为objdump会打印数据部分 - 如果你转动它们,你就得到print[0-4]()的函数地址。

编译器通过间接call调用目标 - 即表使用直接在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"); }

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

看起来更像你想要的?

这样做的原因是你需要“无堆栈”功能才能做到这一点 - 尾递归(通过jmp而不是ret从函数返回)只有你要么已经完成了所有堆栈清理,或者不必执行任何操作,因为您无需清理堆栈。编译器可以(但不一定)选择在最后一次函数调用之前进行清理(在这种情况下,最后一次调用可以由jmp进行),但这只有在你返回从该函数获得的值时才有可能。 ,或者如果你“返回void”。并且,如上所述,如果您实际上使用堆栈(就像您的示例对input变量所做的那样)那么没有任何东西可以让编译器强制以尾递归的方式撤消它结果

<强> EDIT2:

第一个示例的反汇编,具有相同的更改(argc而不是input并强制void main - 没有标准一致性评论,请这是一个演示),结果在集会后:

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 <printf@plt>
  40051e:       bf 41 06 40 00          mov    $0x400641,%edi
  400523:       31 c0                   xor    %eax,%eax
  400525:       e9 fe fe ff ff          jmpq   400428 <printf@plt>
  40052a:       bf 46 06 40 00          mov    $0x400646,%edi
  40052f:       31 c0                   xor    %eax,%eax
  400531:       e9 f2 fe ff ff          jmpq   400428 <printf@plt>
  400536:       bf 4a 06 40 00          mov    $0x40064a,%edi
  40053b:       31 c0                   xor    %eax,%eax
  40053d:       e9 e6 fe ff ff          jmpq   400428 <printf@plt>
  400542:       bf 4e 06 40 00          mov    $0x40064e,%edi
  400547:       31 c0                   xor    %eax,%eax
  400549:       e9 da fe ff ff          jmpq   400428 <printf@plt>
  40054e:       90                      nop
  40054f:       90                      nop

这在某种程度上更糟糕(两个 jmp而不是一个)但在另一个方面更好(因为它消除了static函数并内联代码)。优化方面,编译器几乎完成了同样的事情。

答案 4 :(得分:1)

您是否分析过不同的代码?我认为可能会对间接调用 进行优化。以GCC 4.6.1为目标,对x64平台(MinGW)进行了以下分析。

如果你看一下使用jt[input]()时会发生什么,一个调用会导致执行以下代码序列:

  • 间接调用其中一个printX()函数
  • printX()函数设置printf()的参数,然后
  • 跳转到printf()
  • printf()调用将直接返回到“间接通话”的网站。

共有3个分支机构。

当你使用switch语句时会发生什么:

  • 为每种情况间接跳转到一些自定义代码(内联printX()调用)
  • 'case handler'为printf()调用
  • 加载适当的参数
  • 致电printf()
  • printf()调用将返回“案件处理程序”
  • 跳转到交换机的出口点(除了一个案例处理程序,其中退出代码是内联的 - 其他案例跳转到那里)

共有4个分支(一般情况下)。

在这两种情况下你都有:    - 一个间接分支(一个是呼叫,另一个是跳转)    - printf()的分支(一个跳转,另一个跳转)    - 回到呼叫站点的分支

但是,当使用switch语句时,还有一个额外的分支可以到达交换机的“结束”(在大多数情况下)。

现在,如果您实际分析了一些内容,处理器可能会比间接调用更快地处理间接跳转,但我猜想即使是这种情况,基于交换机的代码中使用的附加分支仍然会通过函数指针推动音阶以支持调用。


对于那些感兴趣的人,这里是使用jk[input]();生成的汇编程序(两个示例都是使用GCC MinGW 4.6.1编译,目标是x64,使用的选项是-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

答案 5 :(得分:1)

后一个函数的代码在间接call和后续ret之间不执行任何操作吗?如果间接调用的地址计算使用了一个寄存器,我会不会感到惊讶,该寄存器的值需要保留后一个函数(意味着它必须在计算之前保存该值,并在之后的某个时间恢复它)。虽然可以在间接调用之前移动寄存器恢复代码,但编译器只能在已编程识别为合法机会的情况下执行此类代码运动。

另外,虽然我认为不重要,但我建议例程不应该是inline,因为编译器无法以这种方式执行它们。