C 为什么调用JMP就足够了?
我有两个文件:C 为什么调用JMP就足够了?,c,gcc,assembly,x86,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
#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;
}
#包括
静态内联void print0(){printf(“零”);}
静态内联void print1(){printf(“一”);}
静态内联void print2(){printf(“Two”);}
静态内联void print3(){printf(“三”);}
静态内联void print4(){printf(“四”);}
int main()
{
无符号整数输入;
scanf(“%u”,&input);
开关(输入)
{
案例0:print0();中断;
案例1:print1();中断;
案例2:print2();中断;
案例3:print3();中断;
案例4:print4();中断;
}
返回0;
}
及
#包括
静态内联void print0(){printf(“零”);}
静态内联void print1(){printf(“一”);}
静态内联void print2(){printf(“Two”);}
静态内联void print3(){printf(“三”);}
静态内联void print4(){printf(“四”);}
int main()
{
无符号整数输入;
scanf(“%u”,&input);
静态无效(*jt[])()={print0,print1,print2,print3,print4};
jt[input]();
返回0;
}
我希望它们被编译成几乎相同的汇编代码。在这两种情况下都会生成跳转表,但中的调用由jmp
表示,而中的调用由调用
表示。为什么编译器不优化调用
s?是否可以提示gcc我希望看到jmp
s而不是call
s
使用gcc-Wall-Winline-O3-S-masm=intel编译,gcc版本4.6.2。GCC4.8.0生成的代码稍少,但问题仍然存在
UPD:将
jt
定义为const void(*const jt[])()={print0,print1,print2,print3,print4}代码>并使函数静态常量内联
没有帮助:我的猜测是,这种优化与这样一个事实有关,即在开关后立即有一个return
语句:优化器意识到它可以利用嵌入到print0
。print4
函数,并将调用减少到jmp
;CPU在所选的printN
中点击的ret
作为主
的返回
尝试在开关后插入一些代码,看看编译器是否将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;
}
因为函数指针数组是可变的。编译器已经决定不能假定指针不会被更改。您可能会发现程序集不同于C++,和/或使JT const。 < p>编译器编写者有很多工作要做。显然,他们会优先考虑回报最大、最快的工作
Switch语句在所有类型的代码中都很常见,因此对它们执行的任何优化都会对许多程序产生影响
此代码
jt[input]();
不太常见,因此在编译器设计人员的待办事项列表中要长得多。也许他们还没有(还)发现值得努力去优化它?这会为他们赢得任何已知的基准吗?或者改进一些广泛使用的代码库?您分析过不同的代码吗?我认为可能有人认为间接调用是优化的。以下分析是针对x64平台(MinGW)的GCC 4.6.1进行的
如果查看使用jt[input]()
时发生的情况,调用将导致执行以下代码序列:
- 对
printX()
函数之一的间接调用
- 函数为
printf()
设置参数,然后
- 跳转到
printf()
printf()
调用将直接返回到`间接调用'的站点
总共有3个分支机构
使用switch语句时会发生以下情况:
- 对于每种情况(内联
printX()
calls),间接跳转到一位自定义代码
- “case handler”为
printf()
调用加载适当的参数
- 调用
printf()
printf()
调用将返回到
- 跳转到开关的退出点(除了一个内联退出代码的案例处理程序-其他案例跳转到那里)
总共4个分支(一般情况下)
在这两种情况下,您都有:
-间接分支(一个是呼叫,另一个是跳转)
-到printf()
的分支(一个是跳转,另一个是调用)
-返回呼叫站点的分支
但是,当使用switch
语句时,会有一个额外的分支到达开关的“末端”(在大多数情况下)
现在,如果你真的分析了一些东西,处理器处理间接跳转的速度可能比间接调用快,但我猜即使是这样,基于开关的代码中使用的额外分支仍然会通过函数指针推动有利于调用的尺度
对于那些感兴趣的人,这里是使用jk[input]()生成的汇编程序代码>(使用GCC MinGW 4.6.1针对x64编译的两个示例,使用的选项是-Wall-Winline-O3-S-masm=intel
):
下面是为基于交换机的实现生成的代码:
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
后一个函数的代码是否在间接调用
和后续ret
之间不起作用?如果间接调用的地址计算使用的寄存器的值需要后一个函数保存(这意味着它必须在计算之前保存该值,并在计算之后恢复该值),我不会感到惊讶。虽然可以在间接调用之前移动寄存器还原代码,但编译器只能在已编程为识别为合法机会的情况下执行此类代码移动
另外,虽然我认为这不重要,但我还是建议
jt[input]();
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
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 ... ]
jmp <to location that sets arg for printf()>
...
jmp <back to common location for the printf() invocation>
...
call <printf>
...
retq
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 ..@.....
#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]();
}
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
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