首页 > 解决方案 > 为什么 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   
                                  <

标签: cgccassemblyshared-librariesx86-64

解决方案


调用不能内联(因为您将定义放在单独的文件中并且没有使用链接时优化)。

我认为您正在测量在传统 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符号可见性。


推荐阅读