我有两个文件:
#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
答案 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()
调用)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
,因为编译器无法以这种方式执行它们。