c - 为什么 gcc 的 switch 生成的跳转比等效的函数调用更快,但只有静态链接?
问题描述
我很好奇 switch 跳转相对于函数调用的执行情况,所以我做了一个快速的基准测试:
#!/bin/bash -eu
cat > get.c <<EOF
#include <stdint.h>
int get(int (Getter)(void))
{
uintptr_t getter=(uintptr_t)Getter;
if(1){
switch(getter){
case 0: return $RANDOM;
case 1: return $RANDOM;
case 2: return $RANDOM;
case 3: return $RANDOM;
case 4: return $RANDOM;
case 5: return $RANDOM;
default: return Getter();
}
}else{
if(0==getter) return $RANDOM;
else if(1==getter) return $RANDOM;
else if(2==getter) return $RANDOM;
else if(3==getter) return $RANDOM;
else if(4==getter) return $RANDOM;
else if(5==getter) return $RANDOM;
else return Getter();
}
}
EOF
cat > main.c <<EOF
int get(int (Getter)(void));
int Getter(void){ return 42; }
int main(int C, char**V)
{
if(C==1)
for(int i=0; i<1000000000;i++)
get((int(*)(void))4);
else
for(int i=0; i<1000000000;i++)
get(Getter);
}
EOF
: ${CC:=gcc}
arg='-Os -fpic'
for c in *.c; do $CC $arg -c $c; done
$CC get.o -o libget.so -shared
$CC main.o $PWD/libget.so -o dso
$CC main.o get.o -o dso -o static
set -x
time ./dso
time ./dso 1
time ./static
time ./static 1
时间(相对稳定)是:
+ ./dso
real 0m3.778s
user 0m3.709s
sys 0m0.056s
+ ./dso 1
real 0m3.739s
user 0m3.736s
sys 0m0.000s
+ ./static
real 0m2.478s
user 0m2.477s
sys 0m0.000s
+ ./static 1
real 0m3.425s
user 0m3.411s
sys 0m0.000s
为什么 switch 跳转的性能要好得多,但只有在函数是静态链接的时候?
分别为动态版和静态版的反汇编diff(sdiff-generated):
000000000000111a <get>: | 0000000000001180 <get>:
cmp $0xc,%rdi cmp $0xc,%rdi
ja 1178 <get+0x5e> | ja 11de <get+0x5e>
lea 0xed9(%rip),%rdx # 2000 <_fini+0xe80> | lea 0xe77(%rip),%rdx # 2004 <_IO_stdin_used
movslq (%rdx,%rdi,4),%rax movslq (%rdx,%rdi,4),%rax
add %rdx,%rax add %rdx,%rax
jmpq *%rax jmpq *%rax
mov $0x132b,%eax mov $0x132b,%eax
retq retq
mov $0x2740,%eax mov $0x2740,%eax
retq retq
mov $0x79b6,%eax mov $0x79b6,%eax
retq retq
mov $0x5234,%eax mov $0x5234,%eax
retq retq
mov $0x6389,%eax mov $0x6389,%eax
retq retq
mov $0x37de,%eax mov $0x37de,%eax
retq retq
mov $0x6a22,%eax mov $0x6a22,%eax
retq retq
mov $0x1a35,%eax mov $0x1a35,%eax
retq retq
mov $0x2ce8,%eax mov $0x2ce8,%eax
retq retq
mov $0x4fed,%eax mov $0x4fed,%eax
retq retq
mov $0xfe3,%eax mov $0xfe3,%eax
retq retq
mov $0x4229,%eax mov $0x4229,%eax
retq retq
jmpq *%rdi jmpq *%rdi
mov $0x529e,%eax mov $0x529e,%eax
retq retq
<
解决方案
调用不能内联(因为您将定义放在单独的文件中并且没有使用链接时优化)。
我认为您正在测量在传统 Unix 风格的共享库中调用函数时通过 PLT 调用的额外开销,而 gcc 默认情况下会这样做。 用于-fno-plt
发出直接使用 GOT 条目的内存间接调用指令,而不是call
使用 memory-indirect jmp
。有关 PLT 开销的更多信息,请参阅Linux 上动态库的抱歉状态,或自行反汇编。(TODO:在这个答案中添加反汇编。)
我希望-fno-plt
这两个版本的运行几乎完全相同。
" 的两个版本的 asm"get
是相同的,以不同的随机数和不同的地址为模。它们可能执行相同,但都很慢,因为 gcc 错过了将其switch
转换为表查找的优化。请参阅https://gcc.gnu.org/ bugzilla/show_bug.cgi?id=85585和相关的东西。(顺便说一句,gcc 将表压缩为偏移量,而不是使用原始指针的经典跳转表,因为它试图避免无处不在的绝对地址,即使是数据。一些目标不'即使这样也不支持修复,gcc 目前甚至在像 x86-64/Linux 这样的目标上也避免了它们,在这些目标上,它可以很好地使用运行时修复。但当然,做一个间接分支而不是仅仅在一个在这种情况下的表。)
还相关:x86-64 Linux 中不再允许使用 32 位绝对地址?谈论一些关于 和 的-fpie
成本-fpic
。在这种情况下,没有什么可以通过省略-fpic
和/或使用来保存-fno-pie -no-pie
,因为单独的文件也阻止了函数内联,而不仅仅是可能的符号插入/ELF符号可见性。
推荐阅读
- javascript - 分组中的Ag网格显示
- java - Spring 安全主体不适用于 @PostConstruct
- javascript - 显示货币输入表单,无需先点击
- git - 如何重置一些提交和合并(通过从源拉取)以再次从远程拉取?
- git - Google 的基于主干的开发 - 您是否直接推送代码以发布分支而不是主干?
- java - java.lang.IllegalArgumentException:日志标记“okhttp3.mockwebserver.MockWebServer”超过 23 个字符的限制
- javascript - 为什么 window.btoa 不能处理 Javascript 中的“-”字符?
- node.js - 在 postgres 中插入或更新时,我得到 Parser.parseErrorMessage
- java - Spring MongoRepository Null 和 isNull 的区别
- java - Android viewGroup 问题 - 使用 Appium 进行测试