C语言 “切换”比“如果”快吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6805026/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 09:12:59  来源:igfitidea点击:

Is 'switch' faster than 'if'?

cperformanceswitch-statementassemblyjump-table

提问by user541686

Is a switchstatement actuallyfaster than an ifstatement?

是一种switch说法实际上比更快的if声明?

I ran the code below on Visual Studio 2010's x64 C++ compiler with the /Oxflag:

我在带有/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());
}

and got these results:

并得到这些结果:

Switch statement: 5261 ms
If statement: 5196 ms

Switch 语句:5261 ms
If 语句:5196 ms

From what I've learned, switchstatements apparently use jump tables to optimize the branching.

据我所知,switch语句显然使用跳转表来优化分支。

Questions:

问题:

  1. What would a basic jump table look like, in x86 or x64?

  2. Is this code using a jump table?

  3. Why is there no performance difference in this example? Is there any situation in which there isa significant performance difference?

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

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

  3. 为什么在这个例子中没有性能差异?有没有在其中有什么情况一个显著的性能差异?



Disassembly of the code:

代码反汇编:

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       


Update:

更新:

Interesting results here. Not sure why one is faster and one is slower, though.

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

采纳答案by Billy ONeal

There are several optimizations a compiler canmake on a switch. I don't think the oft-mentioned "jump-table" is a very useful one though, as it only works when the input can be bounded some way.

编译器可以对开关进行多种优化。我不认为经常提到的“跳转表”是一个非常有用的,因为它只在输入可以以某种方式有界时才有效。

C Pseudocode for a "jump table" would be something like this-- note that the compiler in practice would need to insert some form of if test around the table to ensure that the input was valid in the table. Note also that it only works in the specific case that the input is a run of consecutive numbers.

“跳转表”的 C 伪代码将是这样的——请注意,实际上编译器需要在表周围插入某种形式的 if 测试,以确保输入在表中有效。另请注意,它仅适用于输入是连续数字的特定情况。

If the number of branches in a switch is extremely large, a compiler can do things like using binary search on the values of the switch, which (in my mind) would be a much more useful optimization, as it does significantly increase performance in some scenarios, is as general as a switch is, and does not result in greater generated code size. But to see that, your test code would need a LOT more branches to see any difference.

如果 switch 中的分支数量非常大,编译器可以对 switch 的值使用二进制搜索,这(在我看来)将是一个更有用的优化,因为它确实显着提高了某些方面的性能场景,与 switch 一样通用,并且不会导致更大的生成代码大小。但是要看到这一点,您的测试代码需要更多的分支才能看到任何差异。

To answer your specific questions:

要回答您的具体问题:

  1. Clang generates one that looks like this:

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    , %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.
  1. Clang 生成一个看起来像这样的

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    , %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. 我可以说它没有使用跳转表——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) 
    

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

  3. 要么没有足够的分支导致编译器生成跳转表,要么您的编译器根本不生成它们。我不确定是哪个。

EDIT 2014: There has been some discussion elsewhere from people familiar with the LLVM optimizer saying that the jump table optimization can be important in many scenarios; e.g. in cases where there is an enumeration with many values and many cases against values in said enumeration. That said, I stand by what I said above in 2011 -- too often I see people thinking "if I make it a switch, it'll be the same time no matter how many cases I have" -- and that's completely false. Even with a jump table you get the indirect jump cost and you pay for entries in the table for each case; and memory bandwidth is a Big Deal on modern hardware.

编辑 2014 年:熟悉 LLVM 优化器的人在其他地方进行了一些讨论,称跳转表优化在许多情况下都很重要;例如,在存在具有许多值的枚举以及针对所述枚举中的值的许多情况的情况下。也就是说,我支持我在 2011 年所说的话——我经常看到人们在想“如果我改变它,无论我有多少病例,时间都会相同”——这完全是错误的。即使使用跳转表,您也会获得间接跳转成本,并为每种情况支付表中的条目;内存带宽对现代硬件来说是一个大问题。

Write code for readability. Any compiler worth its salt is going to see an if / else if ladder and transform it into equivalent switch or vice versa if it would be faster to do so.

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

回答by crypted

To your question:

对于你的问题:

1.What would a basic jump table look like, in x86 or x64?

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

Jump table is memory address that holds pointer to the labels in something like array structure. following example will help you understand how jump tables are laid out

跳转表是内存地址,它保存指向数组结构中标签的指针。以下示例将帮助您了解跳转表的布局方式

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

在此处输入图片说明

Where 00B14538is the pointer to the Jump table , and value like D8 09 AB 00represents label pointer.

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

2.Is this code using a jump table?No in this case.

2.这段代码是否使用了跳转表?在这种情况下没有。

3.Why is there no performance difference in this example?

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

There is no performance difference because instruction for both case looks same, no jump table.

没有性能差异,因为两种情况的指令看起来相同,没有跳转表。

4.Is there any situation in which there is a significant performance difference?

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

If you have very long sequence of ifcheck, in that case using a jump table improves performance (branching/jmp instructions are expensiveif they don't predict near-perfectly) but comes with the cost of memory.

如果你有很长的if检查序列,在这种情况下,使用跳转表可以提高性能(如果分支/jmp 指令不能近乎完美地预测,那么它们会很昂贵),但会带来内存成本。

The code for all the compare instructions has some size, too, so especially with 32-bit pointers or offsets, a single jump table lookup might not cost a lot more size in an executable.

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

Conclusion: Compiler is smart enough handle such case and generate appropriate instructions :)

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

回答by Soren

The compiler is free to compile the switch statement as a code which is equivalent to if-statement, or to create a jump table. It will likely chose one on the other based on what will execute fastest or generate the smallest code somewhat depending on what you have specified in you compiler options -- so worst case it will be the same speed as if-statements

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

I would trust the compiler to do the best choice and focus on what makes the code most readable.

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

If the number of cases becomes very large a jump table will be much faster than a series of if. However if the steps between the values is very large, then the jump table can become large, and the compiler may choose not to generate one.

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

回答by BobTurbo

How do you know your computer was not performing some task unrelated to the test during the switch test loop and performing fewer tasks during the if test loop? Your test results do not show anything as:

您如何知道您的计算机在 switch 测试循环期间没有执行与测试无关的任务,而在 if 测试循环期间执行的任务较少?您的测试结果未显示任何内容:

  1. the difference is very small
  2. there is only one result, not a series of results
  3. there are too few cases
  1. 差别很小
  2. 只有一个结果,而不是一系列结果
  3. 病例太少

My results:

我的结果:

I addded:

我补充说:

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

to the end so that it would not optimise away the loop as counter was never used in your example so why would the compiler perform the loop? Immediately, the switch was always winning even with such a micro-benchmark.

到最后,这样它就不会优化循环,因为您的示例中从未使用过计数器,那么编译器为什么要执行循环?立即,即使有这样一个微基准,转换总是获胜。

The other problem with your code is:

您的代码的另一个问题是:

switch (counter % 4 + 1)

in your switch loop, versus

在您的开关循环中,与

const size_t c = counter % 4 + 1; 

in your if loop. Very big difference if you fix that. I believe that putting the statement inside the switch statement provokes the compiler into sending the value directly into the CPU registers rather than putting it on the stack first. This is therefore in favour of the switch statement and not a balanced test.

在你的 if 循环中。如果你解决这个问题会有很大的不同。我相信将语句放在 switch 语句中会促使编译器将值直接发送到 CPU 寄存器中,而不是先将它放在堆栈上。因此,这有利于 switch 语句而不是平衡测试。

Oh and I think you should also reset counter between tests. In fact, you probably should be using some kind of random number instead of +1, +2, +3 etc, as it will probably optimise something there. By random number, I mean a number based on the current time, for example. Otherwise, the compiler could turn both of your functions into one long math operation and not even bother with any loops.

哦,我认为您还应该在测试之间重置计数器。事实上,您可能应该使用某种随机数而不是 +1、+2、+3 等,因为它可能会优化那里的某些内容。例如,随机数是指基于当前时间的数字。否则,编译器可能会将您的两个函数都转换为一个长数学运算,甚至不会打扰任何循环。

I have modified Ryan's code just enough to make sure the compiler couldn't figure things out before the code had run:

我已经修改了 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

开关:3740
如果:3980

(similar results over multiple attempts)

(多次尝试的类似结果)

I also reduced the number of cases/ifs to 5 and the switch function still won.

我还将 case/ifs 的数量减少到 5 个,并且 switch 功能仍然获胜。

回答by Igor Skochinsky

A good optimizing compiler such as MSVC can generate:

一个好的优化编译器比如 MSVC 可以生成:

  1. a simple jump table if the cases are arranged in a nice long range
  2. a sparse (two-level) jump table if there are many gaps
  3. a series of ifs if the number of cases is small or the values are not close together
  4. a combination of above if the cases represent several groups of closely-spaced ranges.
  1. 一个简单的跳转表,如果案例排列在一个很好的远距离
  2. 如果有很多间隙,则为稀疏(两级)跳转表
  3. 如果个案数量很少或值不靠在一起,则为一系列 if
  4. 如果案例代表几组紧密间隔的范围,则为上述组合。

In short, if the switch looks to be slower than a series of ifs, the compiler might just convert it to one. And it's likely to be not just a sequence of comparisons for each case, but a binary search tree. See herefor an example.

简而言之,如果 switch 看起来比一系列 if 慢,编译器可能只是将它转换为一个。它很可能不仅仅是每个案例的比较序列,而是一个二叉搜索树。有关示例,请参见此处

回答by Bill Forster

I'll answer 2) and make some general comments. 2) No, there is no jump table in the assembly code you've posted. A jump table is a table of jump destinations, and one or two instructions to jump directly to an indexed location from the table. A jump table would make more sense when there are many possible switch destinations. Maybe the optimiser knows that simple if else logic is faster unless the number of destinations is greater than some threshold. Try your example again with say 20 possibilities instead of 4.

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

回答by Ryan Gross

I was intrigued, and took a look at what I could change about your example to get it to run the switch statement faster.

我很感兴趣,并查看了我可以对您的示例进行哪些更改以使其更快地运行 switch 语句。

If you get to 40 if statements, and add a 0 case, then the if block will run slower than the equivalent switch statement. I have the results here: https://www.ideone.com/KZeCz.

如果达到 40 个 if 语句,并添加 0 个 case,那么 if 块的运行速度将比等效的 switch 语句慢。我在这里有结果:https: //www.ideone.com/KZeCz

The effect of removing the 0 case can be seen here: https://www.ideone.com/LFnrX.

去除 0 大小写的效果可以在这里看到:https: //www.ideone.com/LFnrX

回答by Jerry Coffin

Here are some results from the old (now hard to find) bench++ benchmark:

以下是旧的(现在很难找到)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

What we can see from this is that (on this machine, with this compiler -- VC++ 9.0 x64), each iftest takes about 0.7 nanoseconds. As the number of tests goes up, the time scales almost perfectly linearly.

从这里我们可以看到(在这台机器上,使用这个编译器——VC++ 9.0 x64),每个if测试大约需要 0.7 纳秒。随着测试数量的增加,时间几乎完美地线性扩展。

With the switch statement, there's almostno difference in speed between a 2-way and a 10-way test, as long as the values are dense. The 10-way test with sparse values takes about 1.6x as much time as the 10-way test with dense values -- but even with sparse values, still better than twice the speed of a 10-way if/else if.

使用 switch 语句,只要值是密集的,2 路测试和 10 路测试之间的速度几乎没有区别。使用稀疏值的 10 向测试花费的时间大约是使用密集值的 10 向测试的 1.6 倍——但即使使用稀疏值,仍然比 10 向if/的速度好两倍else if

Bottom line: using only a 4-way test won't really show you muchabout the performance of switchvs if/else. If you look at the numbers from this code, it's pretty easy to interpolate the fact that for a 4-way test, we'd expect the two to produce prettysimilar results (~2.8 nanoseconds for an if/else, ~2.0 for switch).

底线:只使用了4路测试将不能真正告诉你很多关于性能switchVS if/ else。如果您查看此代码中的数字,很容易推断出以下事实:对于 4 路测试,我们希望两者产生非常相似的结果(对于if/约 2.8 纳秒,对于else约 2.0纳秒switch)。

回答by Brian Kennedy

Note that when a switch is NOT compiled to a jump table, you can very often write if's more efficiently than the switch...

请注意,当开关未编译为跳转表时,您通常可以编写 if 比开关更有效...

(1) if the cases have an ordering, rather than the worst case testing for all N, you can write your if's to test if in the upper or lower half, then in each half of that, binary search style... resulting in the worst case being logN rather than N

(1) 如果案例有一个排序,而不是所有 N 的最坏情况测试,你可以编写你的 if 来测试是否在上半部分或下半部分,然后在每一半中,二分搜索风格......导致最坏的情况是 logN 而不是 N

(2) if certain cases/groups are far more frequent than other cases, then designing your if's to isolate those cases first can speed up the average time through

(2) 如果某些案例/组比其他案例更频繁,那么首先设计 if 来隔离这些案例可以加快平均时间

回答by Nemo

Not sure why one is faster and one is slower, though.

不过,不确定为什么一个更快,一个更慢。

That is actually not too hard to explain... If you remember that mispredicted branches are tens to hundreds of times more expensive than correctly predicted branches.

这实际上并不太难解释......如果你还记得错误预测的分支比正确预测的分支贵几十到几百倍。

In the % 20version, the first case/if is always the one that hits. Modern CPUs "learn" which branches are usually taken and which are not, so they can easily predict how this branch will behave on almost every iteration of the loop. That explains why the "if" version flies; it never has to execute anything past the first test, and it (correctly) predicts the result of that test for most of the iterations. Obviously the "switch" is implemented slightly differently -- perhaps even a jump table, which can be slow thanks to the computed branch.

% 20版本中,第一个 case/if 总是命中的那个。现代 CPU“学习”通常采用哪些分支,哪些不采用,因此它们可以轻松预测该分支在几乎每次循环迭代中的行为。这就解释了为什么“if”版本飞起来了;它永远不必执行第一个测试之后的任何事情,并且它(正确地)预测了大多数迭代的测试结果。显然,“开关”的实现方式略有不同——甚至可能是一个跳转表,由于计算分支,它可能会很慢。

In the % 21version, the branches are essentially random. So not only do many of them execute every iteration, the CPU cannot guess which way they will go. This is the case where a jump table (or other "switch" optimization) is likely to help.

% 21版本中,分支基本上是随机的。因此,它们中的许多不仅每次迭代都执行,而且 CPU 无法猜测它们将走哪条路。在这种情况下,跳转表(或其他“开关”优化)可能会有所帮助。

It is very hard to predict how a piece of code is going to perform with a modern compiler and CPU, and it gets harder with every generation. The best advice is "don't even bother trying; always profile". That advice gets better -- and the set of people who can ignore it successfully gets smaller -- every year.

很难预测一段代码将如何使用现代编译器和 CPU 执行,而且每一代都变得更加困难。最好的建议是“别费心去尝试;总是配置文件”。这条建议会变得更好——并且能够成功忽略它的人越来越少——每年。

All of which is to say that my explanation above is largely a guess. :-)

所有这些都是说我上面的解释很大程度上是一种猜测。:-)