为什么叫jmps就足够了?

Jou*_*usi 11 c x86 assembly gcc

我有两个文件:

#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;
}
Run Code Online (Sandbox Code Playgroud)

#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;
}
Run Code Online (Sandbox Code Playgroud)

我希望它们被编译成几乎相同的汇编代码.在这两种情况下都会生成跳转表,但第一个文件jmp的调用由表示,而第二个文件中的调用由call.为什么编译器不优化calls?是否有可能暗示gcc,我想看到jmps而不是calls?

编译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

Bo *_*son 8

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

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

这段代码

jt[input](); 
Run Code Online (Sandbox Code Playgroud)

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


Mat*_*ner 5

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


Fra*_*kH. 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 ... ]
Run Code Online (Sandbox Code Playgroud)

请注意,.rodata内容@4006b8是打印的网络字节顺序(无论出于何种原因......),该值40058e位于main上面 - arg-initializer/jmp块开始的位置。那里的所有mov/对都使用八个字节,因此是间接的。在这种情况下,顺序是:jmp(,%rax,8)

jmp <to location that sets arg for printf()>
...
jmp <back to common location for the printf() invocation>
...
call <printf>
...
retq
Run Code Online (Sandbox Code Playgroud)

这意味着编译器实际上已经优化了调用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                    ..@.....
Run Code Online (Sandbox Code Playgroud)

再次注意 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]();
}
Run Code Online (Sandbox Code Playgroud)

创建的程序集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
Run Code Online (Sandbox Code Playgroud)

哪个看起来更像你想要的?

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

编辑2:

第一个示例的反汇编,具有相同的更改(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
Run Code Online (Sandbox Code Playgroud)

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