'开关'比'如果'更快?

switch语句实际上比if语句快吗?

我使用/Ox标志在Visual Studio 2010的x64 C ++编译器上运行以下代码:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 29)
size_t counter = 0;

size_t testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        switch (counter % 4 + 1)
        {
            case 1: counter += 4; break;
            case 2: counter += 3; break;
            case 3: counter += 2; break;
            case 4: counter += 1; break;
        }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

size_t testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = counter % 4 + 1;
        if (c == 1) { counter += 4; }
        else if (c == 2) { counter += 3; }
        else if (c == 3) { counter += 2; }
        else if (c == 4) { counter += 1; }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    printf("Starting...n");
    printf("Switch statement: %u msn", testSwitch());
    printf("If     statement: %u msn", testIf());
}

并得到了这些结果:

开关语句:5261毫秒
如果陈述:5196毫秒

从我所了解到的, switch语句显然使用跳转表来优化分支。

问题:

  • 在x86或x64中,基本跳转表会是什么样子?

  • 这段代码是否使用跳转表?

  • 为什么在这个例子中没有性能差异? 是否存在性能差异显着的情况?


  • 代码的反汇编:

    testIf:
    
    13FE81B10 sub  rsp,48h 
    13FE81B14 call qword ptr [__imp_clock (13FE81128h)] 
    13FE81B1A mov  dword ptr [start],eax 
    13FE81B1E mov  qword ptr [i],0 
    13FE81B27 jmp  testIf+26h (13FE81B36h) 
    13FE81B29 mov  rax,qword ptr [i] 
    13FE81B2E inc  rax  
    13FE81B31 mov  qword ptr [i],rax 
    13FE81B36 cmp  qword ptr [i],20000000h 
    13FE81B3F jae  testIf+0C3h (13FE81BD3h) 
    13FE81B45 xor  edx,edx 
    13FE81B47 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81B4E mov  ecx,4 
    13FE81B53 div  rax,rcx 
    13FE81B56 mov  rax,rdx 
    13FE81B59 inc  rax  
    13FE81B5C mov  qword ptr [c],rax 
    13FE81B61 cmp  qword ptr [c],1 
    13FE81B67 jne  testIf+6Dh (13FE81B7Dh) 
    13FE81B69 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81B70 add  rax,4 
    13FE81B74 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81B7B jmp  testIf+0BEh (13FE81BCEh) 
    13FE81B7D cmp  qword ptr [c],2 
    13FE81B83 jne  testIf+89h (13FE81B99h) 
    13FE81B85 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81B8C add  rax,3 
    13FE81B90 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81B97 jmp  testIf+0BEh (13FE81BCEh) 
    13FE81B99 cmp  qword ptr [c],3 
    13FE81B9F jne  testIf+0A5h (13FE81BB5h) 
    13FE81BA1 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81BA8 add  rax,2 
    13FE81BAC mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81BB3 jmp  testIf+0BEh (13FE81BCEh) 
    13FE81BB5 cmp  qword ptr [c],4 
    13FE81BBB jne  testIf+0BEh (13FE81BCEh) 
    13FE81BBD mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81BC4 inc  rax  
    13FE81BC7 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81BCE jmp  testIf+19h (13FE81B29h) 
    13FE81BD3 call qword ptr [__imp_clock (13FE81128h)] 
    13FE81BD9 sub  eax,dword ptr [start] 
    13FE81BDD imul eax,eax,3E8h 
    13FE81BE3 cdq       
    13FE81BE4 mov  ecx,3E8h 
    13FE81BE9 idiv eax,ecx 
    13FE81BEB cdqe      
    13FE81BED add  rsp,48h 
    13FE81BF1 ret       
    

    testSwitch:
    
    13FE81C00 sub  rsp,48h 
    13FE81C04 call qword ptr [__imp_clock (13FE81128h)] 
    13FE81C0A mov  dword ptr [start],eax 
    13FE81C0E mov  qword ptr [i],0 
    13FE81C17 jmp  testSwitch+26h (13FE81C26h) 
    13FE81C19 mov  rax,qword ptr [i] 
    13FE81C1E inc  rax  
    13FE81C21 mov  qword ptr [i],rax 
    13FE81C26 cmp  qword ptr [i],20000000h 
    13FE81C2F jae  testSwitch+0C5h (13FE81CC5h) 
    13FE81C35 xor  edx,edx 
    13FE81C37 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81C3E mov  ecx,4 
    13FE81C43 div  rax,rcx 
    13FE81C46 mov  rax,rdx 
    13FE81C49 inc  rax  
    13FE81C4C mov  qword ptr [rsp+30h],rax 
    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
    13FE81C71 jmp  testSwitch+0C0h (13FE81CC0h) 
    13FE81C73 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81C7A add  rax,4 
    13FE81C7E mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81C85 jmp  testSwitch+0C0h (13FE81CC0h) 
    13FE81C87 mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81C8E add  rax,3 
    13FE81C92 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81C99 jmp  testSwitch+0C0h (13FE81CC0h) 
    13FE81C9B mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81CA2 add  rax,2 
    13FE81CA6 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81CAD jmp  testSwitch+0C0h (13FE81CC0h) 
    13FE81CAF mov  rax,qword ptr [counter (13FE835D0h)] 
    13FE81CB6 inc  rax  
    13FE81CB9 mov  qword ptr [counter (13FE835D0h)],rax 
    13FE81CC0 jmp  testSwitch+19h (13FE81C19h) 
    13FE81CC5 call qword ptr [__imp_clock (13FE81128h)] 
    13FE81CCB sub  eax,dword ptr [start] 
    13FE81CCF imul eax,eax,3E8h 
    13FE81CD5 cdq       
    13FE81CD6 mov  ecx,3E8h 
    13FE81CDB idiv eax,ecx 
    13FE81CDD cdqe      
    13FE81CDF add  rsp,48h 
    13FE81CE3 ret       
    

    更新:

    这里有趣的结果。 不知道为什么一个更快,一个更慢。


    编译器可以在交换机上进行几次优化。 我不认为经常提到的“跳转表”是一个非常有用的跳转表,因为它只有在输入可以以某种方式限制时才起作用。

    对于“跳转表”,C伪代码应该是这样的 - 请注意,编译器在实践中需要在表中插入某种形式的if test,以确保输入在表中有效。 还要注意,它只适用于输入是连续数字的特定情况。

    如果交换机中的分支数量非常大,编译器可以执行诸如在交换机的值上使用二分搜索的操作,这在我看来是一种更有用的优化,因为它可以显着提高某些交换机的性能场景,与交换机一样通用,并且不会导致更大的生成代码大小。 但要看到这一点,你的测试代码将需要很多分支来查看任何差异。

    回答你的具体问题:

  • 铿锵生成一个看起来像这样的:

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
    
  • 我可以说它没有使用跳转表 - 4条比较指令清晰可见:

    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
    

    基于跳转表的解决方案根本不使用比较。

  • 没有足够的分支导致编译器生成跳转表,或者编译器根本无法生成它们。 我不确定哪个。
  • 2014年编辑 :在其他地方已经有人从熟悉LLVM优化器的人那里进行了一些讨论,他们说跳转表优化在很多场景中都很重要; 例如在所述枚举中枚举具有许多值和许多情况下针对值的情况下。 也就是说,我坚持2011年以上所说的话 - 我经常看到有人在思考“如果我把它作为开关,那么无论我有多少案件,都会同时发生” - 这完全是错误的。 即使使用跳转表,您也可以获得间接跳转成本,并为每种情况支付表中的条目; 内存带宽是现代硬件的一大特色。

    编写可读性代码。 任何值得使用的编译器都会看到if / else if梯形图,并将其转换为等效开关,反之亦然,如果这样做会更快。


    对你的问题:

    1.在x86或x64中,基本跳转表会是什么样子?

    跳转表是内存地址,它持有类似于数组结构的标签的指针。 以下示例将帮助您了解跳转表的外观

    00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
    00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
    00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    

    其中00B14538是跳转表的指针,并且像D8 09 AB 00这样的值代表标签指针。

    2.这个代码使用跳转表吗? 没有在这种情况下。

    3.为什么这个例子没有性能差异?

    没有性能差异,因为两种情况下的指令看起来都一样,没有跳转表。

    4.是否存在显着的性能差异?

    如果你有很长的检查顺序, 那么使用跳转表会降低性能,但是会伴随内存成本。

    座右铭:编译器足够聪明处理这种情况:)


    编译器可以自由地将switch语句编译为等价于if语句的代码,或者创建一个跳转表。 它可能会根据执行速度最快的代码选择一个,或者根据您在编译器选项中指定的内容来生成最小的代码 - 最坏的情况下它将与if语句的速度相同

    我相信编译器会做出最佳选择,并专注于使代码具有最高可读性的原因。

    如果案例数量变得非常大,跳转表将比一系列if快得多。 但是,如果这些值之间的步骤非常大,那么跳转表可能会变大,编译器可能会选择不生成一个跳转表。

    链接地址: http://www.djcxy.com/p/86593.html

    上一篇: Is 'switch' faster than 'if'?

    下一篇: Fastest way to determine if an integer's square root is an integer