“switch”比“if”更快吗?

282

使用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 ms\n", testSwitch());
    printf("If     statement: %u ms\n", testIf());
}

结果如下:

Switch语句:5261毫秒
If语句:5196毫秒

据我所知,switch语句会使用跳转表进行优化。

问题:

  1. x86或x64中基本的跳转表长什么样子?

  2. 这段代码是否使用了跳转表?

  3. 为什么在此示例中没有性能差异?是否存在可以显著影响性能的情况?


该代码的反汇编:

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       

更新:

有趣的结果在这里。虽然不确定为什么一个更快,一个更慢。


65
这些人为什么要投票关闭这个想法?他们是否坚信完美优化编译器的概念,认为其生成不理想代码的任何想法都是异端邪说?他们是否会对“任何地方”的任何优化都感到反感? - Crashworks
10
这个问题到底有什么问题? - Tugrul Ates
26
对于任何想知道这个问题有什么问题的人:首先,这不是一个问题,而是3个问题,这意味着很多答案现在都涉及不同的问题。这意味着很难接受回答“所有问题”的答案。此外,对于上述问题的典型反应是将其关闭为“并不真的很有趣”,这主要是因为在优化的这个级别上,“你几乎总是过早地进行了优化”。最后,5196与5261不应该足以真正关心。编写有意义的逻辑代码即可。 - Lasse V. Karlsen
54
@Lasse:你真的希望我在 Stack Overflow 上发布三个问题吗? 另外:"5196与5261之间不足以引起实际关注" --> 我不确定你是否误解了问题,还是我误解了你的评论,但我的问题的整个重点不就是要问为什么没有差异吗?(我有声称这是一个值得关注的显著差异吗?) - user541686
7
@Robert:这篇文章的评论数量超过20条,但其中大部分是元评论。实际上只有7条评论与问题相关。 观点:我不明白这里有什么“观点”。我没有看到有任何表现差异的原因,难道只是口味不同吗? 辩论:也许吧,但在我看来,这看起来像一个健康的辩论,就像我在Stack Overflow上看到的其他地方一样(如果有什么反对意见,请告诉我)。 论据:我没有看到任何争议性的内容(除非你把它当作“辩论”的同义词?)。 深入讨论:如果包括这些元评论的话,就可以算作是深入讨论了。 - user541686
显示剩余16条评论
12个回答

145
有几种优化编译器可以在 switch 上进行。然而,我认为经常提到的“跳转表”并不是很有用,因为它只适用于输入可以以某种方式限定的情况。
“跳转表”的 C 伪代码类似于 this -- 请注意,实际上编译器需要在表周围插入某种 if 测试,以确保输入在表中有效。还要注意,它仅适用于输入是一系列连续数字的特定情况。
如果 switch 中的分支数非常大,编译器可以像对 switch 的值使用二进制搜索一样做一些事情,这(在我看来)将是一个更有用的优化,因为它在某些情况下显着提高了性能,与 switch 一样通用,并且不会导致生成的代码大小更大。但是,为了看到这一点,您的测试代码需要有更多的分支才能看到任何差异。
回答您的具体问题:
  1. Clang generates one that looks like this:

    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
    
  2. I can say that it is not using a jump table -- 4 comparison instructions are clearly visible:

    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) 
    

    A jump table based solution does not use comparison at all.

  3. Either not enough branches to cause the compiler to generate a jump table, or your compiler simply doesn't generate them. I'm not sure which.
2014年更新:LLVM优化器的一些熟悉人士在其他地方进行了一些讨论,称跳转表优化在许多场景中都非常重要;例如,在具有许多值和针对该枚举中的值的许多情况的情况下。 也就是说,我仍然坚持我在2011年所说的话 - 太多时候,我看到人们认为“如果我将其改为switch,无论我有多少个案例,它都会花费相同的时间”,这是完全错误的。 即使使用跳转表,您也需要支付间接跳转成本,并为每种情况在表中支付条目;而在现代硬件上,内存带宽是一个大问题。

编写易读的代码。任何值得一试的编译器都会将if / else if梯形结构转换为等效的switch或反之,如果这样做更快的话。


4
谢谢夸奖,我会尽力完成翻译任务。根据我的理解,跳转表使用间接跳转,这个理解是正确的吗?如果是,这是否通常由于更难的预取/流水而导致速度较慢? - user541686
1
@Mehrdad:不幸的是,没有。:( 我很高兴我属于总是认为IF更易读的人的阵营!:) - Billy ONeal
2
一些评论 - "[switches] 只有在输入可以以某种方式被限定时才能工作" "需要在表格周围插入某种 if 测试,以确保输入在表格中有效。还要注意,它仅适用于输入是连续数字的情况。": 完全有可能有一个稀疏填充的表格,在读取潜在指针时只有非 NULL 才执行跳转,否则跳转到默认情况(如果有),然后 switch 退出。阅读此答案后,Soren 说了我想说的其他几件事。 - Tony Delroy
4
任何一款值得用的编译器都会看到一个if / else if的梯子,并将其转换为等效的switch语句,反之亦然。这种说法有支持吗?编译器可能会假设您的if语句已经手动调整过顺序以匹配频率和相对性能需求,而switch通常被视为优化的开放邀请。跳过switch语句是个好主意。代码大小取决于情况/范围 - 可能会更好。最后,某些枚举、位域和char场景本质上是有效/无开销的。 - Tony Delroy
1
@TonyD:我知道已经过了几年,但请注意,Clang为switch和if生成相同的代码:http://goo.gl/VSi2af - Billy ONeal
显示剩余9条评论

50

针对您的问题:

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  ................

enter image description here

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

2.这段代码使用了跳转表吗? 在这个例子中没有。

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

因为两种情况下的指令看起来都相同,没有跳转表。

4.是否有任何情况存在显著的性能差异?

如果您有非常长的if检查序列,在这种情况下使用跳转表可以提高性能(如果不完美预测,则分支/ jmp指令是昂贵的),但代价是内存。

所有比较指令的代码也有一些大小,所以特别是对于32位指针或偏移量,单个跳转表查找在可执行文件中可能不会增加太多大小。

结论:编译器足够聪明,可以处理这种情况并生成适当的指令 :)


最好包括gcc -S输出:.long L1 / .long L2表项序列比十六进制转储更有意义,对于想要学习如何查看编译器的人更有用。(尽管我猜你只需查看开关代码以查看它是否为间接jmp或一堆jcc)。 - Peter Cordes

34

编译器可以将 switch 语句编译成与 if 语句等效的代码,也可以创建一个跳转表。它通常会根据执行速度或生成最小代码的需要选择其中一种,具体取决于您在编译器选项中指定的内容 - 所以最坏情况下它的速度将与 if 语句相同。

我会相信编译器做出最好的选择,并专注于使代码最易读。

如果 case 的数量非常大,则跳转表比一系列 if 快得多。但是,如果值之间的步骤非常大,则跳转表可能会变得很大,编译器可能会选择不生成跳转表。


13
我认为这并没有回答原帖的问题,完全没有。 - Billy ONeal
5
如果那是“基本问题”的话,我就不会费心在问题中的其他179行,而只需要写1行了。 :-) - user541686
9
@Soren:我看到原帖问题中至少有三个有编号的子问题。你只是在宣扬适用于所有“性能”问题的完全相同的答案——即必须先进行测量。考虑到Mehrdad也许已经进行了测量,并且已经将这段代码作为热点进行了隔离。在这种情况下,你的答案不仅毫无价值,而且会产生干扰。 - Billy ONeal
2
根据你的定义,跳转表和非跳转表之间存在模糊的界限。我已经提供了关于子问题第三部分的信息。 - Soren
2
@wnoise:如果这是唯一正确的答案,那么就永远不会有任何性能问题需要问了。然而,在现实世界中,我们中的一些人确实会测量我们的软件,有时候我们不知道如何让一段代码更快,一旦它被测量过了。显然,Mehrdad在提问之前做了一些努力;我认为他的具体问题是可以回答的。 - Billy ONeal
显示剩余2条评论

15
你如何知道在交换测试循环期间,你的计算机没有执行与测试无关的某些任务,并且在if测试循环期间执行了较少的任务?你的测试结果并未显示以下内容:
  1. 差异非常小
  2. 只有一个结果,而不是一系列结果
  3. 案例太少

我的结果:

我添加了:

printf("counter: %u\n", counter);

为了避免编译器将循环优化掉,你需要把循环执行到底,因为在你的示例中counter从未被使用,所以编译器为什么要执行循环呢?立即执行的switch在这种微型基准测试中总是胜出。

你代码的另一个问题是:

switch (counter % 4 + 1)
在您的switch循环中,与之相对的是
const size_t c = counter % 4 + 1; 

在你的if循环中,如果你修复了这个问题会有很大的不同。我认为把语句放在switch语句内部会促使编译器直接将值发送到CPU寄存器中,而不是先将其放入堆栈中。因此,这有利于switch语句,而不是一个平衡的测试。

哦,我认为你应该在测试之间重置计数器。事实上,你可能应该使用某种随机数而不是+1、+2、+3等,因为它可能会优化一些东西。通过随机数,我指的是基于当前时间的数字,例如。否则,编译器可能会将你的两个函数变成一个长的数学运算,甚至不会考虑任何循环。

我已经修改了Ryan的代码,只是让编译器在运行代码之前无法推断出其中的内容:

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

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

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

switch:3740
if:3980

(多次尝试结果相似)

我将case/if的数量减少到5,switch函数仍然获胜。


1
你在哪里添加了 print 语句?我把它添加到整个程序的末尾,但没有看到任何区别。我也不明白另一个程序的“问题”在哪里...能否解释一下其中的“非常大的区别”是什么? - user541686
2
@BobTurbo:45983493超过了12个小时。那是打错字了吗? - Gus
2
太好了,现在我又得再做一遍 :) - BobTurbo
1
@Bob:你没有展示这些测试的任何统计分析... 我看不到任何一致性的表现。 至于小百分比,你展示了一个6%的差异。如果差异更大,我可以认为转换总是更快,但是对于那么小的差异,我怀疑在实际情况下几乎没有区别。 - Billy ONeal
1
@BobTurbo:迟做总比不做好?正如Billy在几个评论前指出的那样,你在循环内部使用了rand()。难道你不应该在开始计时之前将随机数生成到数组中吗?现在,你正在将“rand加上if的成本”与“rand加上switch的成本”进行比较。如果rand所需的时间比ifswitch多得多,你可能会严重削弱比较的效果。 - Mike Dunlavey
显示剩余15条评论

7

以下是一些来自旧版(现在难以找到)bench++基准测试的结果:

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

我们可以从中看出(在这台机器上,使用这个编译器——VC++ 9.0 x64),每个 if 测试大约需要 0.7 纳秒。随着测试数量的增加,时间几乎完美地呈线性比例增长。
对于 switch 语句,在值密集的情况下,2 路和 10 路测试之间几乎没有速度差异。具有稀疏值的 10 路测试所需的时间约为密集值的 10 倍 —— 但即使是稀疏值,仍然比 10 路 if/else if 快两倍以上。
底线:仅使用 4 路测试不会真正展示出 switch 与 if/else 的性能差异。如果您查看此代码的数字,很容易推断出对于 4 路测试,我们预计两者将产生相似的结果(if/else 为 ~2.8 纳秒,switch 为 ~2.0 纳秒)。

1
如果我们不知道测试是否故意寻找一个值,而这个值在if/else链的末尾匹配或者分散在其他地方,那么很难理解它的含义。在谷歌上搜索了10分钟后,无法找到bench++的源代码。 - Tony Delroy

7
一个好的优化编译器,如MSVC,可以生成:
  1. 如果case在一个不错的长范围内,那么它可以生成一个简单的跳转表
  2. 如果有很多间隔,那么它可以生成一个稀疏(两级)跳转表
  3. 如果case数量很少或值不接近,则它可以生成一系列if语句
  4. 如果case代表几个紧密排列的范围组,则它可以结合上面的方法生成。
简而言之,如果switch看起来比一系列ifs慢,编译器可能会将其转换为一个。并且它很可能不仅是每个case的顺序比较,而是一棵二叉查找树。这里有一个例子,请参见这里

1
实际上,编译器也能够使用哈希和跳转来替换它,这比您提出的稀疏两级解决方案性能更好。 - Alice

5

我来回答2)并作一些常规评论。2)不,你发布的汇编代码中没有跳转表。跳转表是一张跳转目的地的表格,并有一到两条指令从表格中直接跳转到索引位置。当存在许多可能的切换目标时,跳转表才更有意义。也许优化器知道简单的 if else 逻辑会更快,除非目标数大于某个阈值。请尝试再次使用你的示例,而不是4种可能性,改为20种。


+1 感谢回答问题 #2! :) (顺便说一下,这里 有更多可能性的结果。) - user541686

4
我很感兴趣,看了一下你的示例,想知道如何更改以使 switch 语句运行更快。
如果您有40个 if 语句,并添加一个0情况,则 if 代码块将比等效的 switch 语句运行得更慢。我在这里展示了结果:https://www.ideone.com/KZeCz
删除0情况的影响在这里可以看到:https://www.ideone.com/LFnrX

2
你的链接已经失效了。 - T.S

3
请注意,如果一个开关没有编译成跳转表,通常情况下,您可以比开关更有效地编写if语句...
(1)如果情况有顺序,而不是最坏情况测试所有N,则可以编写if来测试上半部分或下半部分,然后在每个半部分中进行二进制搜索式的操作... 最坏情况是logN而不是N。
(2)如果某些情况/组远比其他情况重要,那么设计您的if使得首先隔离这些情况可以加快平均处理时间。

1
这是明显不正确的;编译器完全能够进行这两种优化。 - Alice
2
Alice,编译器怎么知道哪些情况在你的预期工作负载中会比其他情况更常见?(A:它不可能知道,因此也无法进行这样的优化。) - Brian Kennedy
1
(1)可以很容易地完成,有些编译器已经使用二分查找来实现了。 (2)可以通过多种方式进行预测或向编译器指示。你从未使用过GCC的“likely”或“unlikely”吗? - Alice
有些编译器允许以收集统计信息的模式运行程序,然后从中进行优化。 - Phil1970

2

这些是if-then跳转,else if-then跳转等。跳转表将具有地址表或使用哈希表或类似的东西。

快与慢是主观的。例如,你可以将case 1放在最后而不是第一位,如果你的测试程序或实际程序大多数情况下使用case 1,则此实现的代码会变慢。因此,只需重新排列case列表,根据实现方式,就可以产生很大的差异。

如果你使用了0-3而不是1-4个case,编译器可能会使用跳转表,编译器应该已经计算出去掉+1的结果。也许是因为项目数量较小。如果你将其设置为0-15或0-31,例如,它可能会使用表格实现或使用其他快捷方式。编译器可以自由选择如何实现事物,只要满足源代码的功能即可。这涉及到编译器的差异、版本差异和优化差异。如果你想要一个跳转表,就制作一个跳转表;如果你想要if-then-else树,就制作一个if-then-else树。如果你想让编译器决定,使用switch/case语句。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接