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:定义jt为const void (* const jt[])() = { print0, print1, print2, print3, print4 };并使功能static const inline无效:http://ideone.com/97SU0
编译器编写者有很多工作要做.显然,他们优先考虑具有最大和最快收益的工作.
Switch语句在各种代码中都很常见,因此对它们执行的任何优化都会对许多程序产生影响.
这段代码
jt[input]();
Run Code Online (Sandbox Code Playgroud)
很少见,因此编译器设计者的TODO列表要长得多.也许他们还没有(还)发现尝试优化它的努力值得吗?这会赢得他们任何已知的基准吗?或者改进一些广泛使用的代码库?
第一种情况(通过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并内联代码)。在优化方面,编译器几乎做了同样的事情。