“切换”比“ if”快吗?


242

是一种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());
}

并得到以下结果:

切换语句:5261 ms
如果语句:5196 ms

据我了解,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       

更新:

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


47
人们到底投票赞成关闭这种想法?他们是不是完全优化编译器的概念的信奉者,认为生成不到理想代码的任何想法都是异端?任何地方进行任何优化的想法是否都会冒犯他们?
Crashworks

6
这个问题到底有什么问题?
Tugrul Ates

25
对于任何想知道这个问题出了什么问题的人:对于初学者来说,这不是一个问题,而是3个问题,这意味着许多答案现在都针对不同的问题。这意味着,这将是难以接受,回答任何回答一切。此外,对上述问题的典型下意识的反应是将其关闭为“并非真的那么有趣”,这主要是由于在此优化级别上,您几乎总是过早地进行优化。最后,5196对5261对不足以实际关心。编写有意义的逻辑代码。
Lasse V. Karlsen

40
@Lasse:您真的希望我在SO上发布三个问题吗?另外:5196 vs. 5261 shouldn't be enough to actually care->我不确定您是不是误解了这个问题,还是我误解了您的评论,但是问题的全部不是要问为什么没有区别吗?(我是否曾经说过要关心这有什么
大不了

5
@Robert:好吧,它只有20条以上的评论,因为它们是元评论。这里实际上只有7条与该问题相关的评论。意见:我看不到这里有什么“意见”。还有一个原因是我没有看到的性能差异,不是吗?只是味道吗?辩论:也许,但是对我来说,这似乎是一种健康的辩论,就像我在SO的其他地方所见(让我知道是否有相反的说法)。论点:我在这里看不到任何争论(除非您将其视为“辩论”的同义词?)。扩展讨论:如果包含这些元注释。
user541686 2011年

Answers:


122

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

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

如果开关中的分支数量非常多,则编译器可以执行一些操作,例如对开关的值使用二进制搜索,(在我看来)这将是非常有用的优化,因为它确实可以显着提高某些应用程序的性能。场景,就像开关一样,不会导致生成的代码更大。但是要看到,您的测试代码将需要更多的分支才能看到任何区别。

要回答您的特定问题:

  1. Clang生成一个看起来像这样的代码

    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. 我可以说它没有使用跳转表-清晰可见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:熟悉LLVM优化器的人在其他地方进行了一些讨论,他们说跳转表优化在许多情况下都非常重要。例如,在存在具有许多值的枚举的情况下,以及针对所述枚举中的值的许多情况下。也就是说,我坚持我在2011年所说的话-我经常看到人们在想“如果我做出改变,无论我有多少案件,都是同一时间”-这完全是错误的。即使有了跳转表,您也可以获取间接跳转成本,并且需要为每种情况在表中支付条目。内存带宽是现代硬件上的大事。

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


3
+1用于实际回答问题以及提供有用的信息。:-)但是,有一个问题:据我了解,跳转表使用间接跳转;那是对的吗?如果是这样,由于难于预取/流水线操作,这通常不会更慢吗?
user541686 2011年

1
@Mehrdad:是的,它使用间接跳转。但是,一个间接跳转(随之而来的管道停顿)可能少于数百个直接跳转。:)
Billy ONeal

1
@Mehrdad:不,很不幸。:(我很高兴我一直在那些总是认为IF更具可读性的人的阵营中!:)
Billy ONeal

1
很少打趣-“ [switches]仅在可以对输入进行某种方式限制时才起作用”“”需要在表周围插入某种形式的if测试以确保输入在表中有效。还要注意,它仅在特定表中有效如果输入是连续的数字。”:完全有可能有一个填充稀疏的表,仅在执行非NULL的情况下才读取潜在的指针,否则将跳转到默认情况,然后switch退出。读完这个答案后,索伦还说了几件事。
Tony Delroy

2
“任何值得付出代价的编译器都会看到一个if / else if梯形图,并将其转换为等效的开关,反之亦然” –对这种断言的支持吗?编译器可能会假设if子句的顺序已经过手动调整,可以匹配频率和相对性能的需求,switch传统上,a 被视为公开邀请您进行优化,但编译器会选择这样做。好点再过去switch:-)。代码大小取决于大小写/范围-可能更好。最后,一些枚举,位字段和char方案本质上是有效/有界的且无开销。
Tony Delroy 2014年

47

对你的问题:

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是指向Jump表的指针,而D8 09 AB 00之类的值表示标签指针。

2.此代码是否使用跳转表? 在这种情况下不可以。

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

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

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

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

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

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


(编辑:nvm,比利的答案已经有了我的建议。我想这是一个很好的补充。)最好包含gcc -S输出:.long L1/ .long L2表条目的序列比十六进制转储更有意义,并且对那些想学习如何看待编译器。(尽管我猜您只是看一下转换代码,看它是间接的jmp还是一堆jcc)。
彼得·科德斯

31

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

我相信编译器会做出最佳选择,并专注于使代码更具可读性的方面。

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


13
我认为这不能回答OP的问题。完全没有
Billy ONeal

5
@索伦:如果那是“基本问题”,那么我就不用理会问题中的其他179行,而只是1行。:-)
user541686 2011年

8
@Soren:我认为OP 中至少有3个编号为子的问题。您只是吹嘘完全相同的答案,该答案适用于所有“性能”问题-即,您必须首先衡量。考虑到Mehrdad可能已经进行了测量,并且将这段代码隔离为一个热点。在这种情况下,您的答案总比一文不值更糟糕,这是噪音。
Billy ONeal

2
在什么是跳转表与什么不取决于跳转定义之间,存在一条模糊的界线。我已提供了有关子问题第3部分的信息
。– Soren

2
@wnoise:如果这是唯一正确的答案,那么就永远没有理由问任何性能问题。但是,在现实世界中,有些人确实在衡量我们的软件,有时候我们一经测度就不知道如何更快地编写一段代码。很明显,梅尔达德在问这个问题之前已经付出了一些努力。而且我认为他的具体问题不但可以回答。
Billy ONeal,

13

您怎么知道您的计算机在切换测试循环中没有执行与测试无关的某些任务,而在if测试循环中没有执行更少的任务?您的测试结果未显示以下任何内容:

  1. 差别很小
  2. 只有一个结果,而不是一系列结果
  3. 案例太少

我的结果:

我补充说:

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

到最后,这样就不会优化循环,因为在您的示例中从未使用过计数器,那么为什么编译器会执行循环?立即,即使有了这样的微基准测试,切换始终是成功的。

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

switch (counter % 4 + 1)

在您的切换循环中,

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);
} 

开关:3740
如果:3980

(多次尝试的结果相似)

我还将案例/条件的数量减少到5,并且仍然可以使用切换功能。


Idk,我无法证明这一点;你得到不同的结果吗?
user541686 2011年

+1:基准测试很困难,并且您真的不能从在普通计算机上一次运行的小时差得出任何结论。您可能会尝试运行大量测试,并对结果进行一些统计。或在仿真器中控制执行时计算处理器周期。
Thomas Padron-McCarthy

嗯,您到底在哪里添加了该print语句?我在整个程序的末尾添加了它,没有发现任何区别。我也不明白另一个问题是什么...介意解释什么是“很大的区别”?
user541686 2011年

1
@BobTurbo:45983493已经超过12小时。那是错字吗?
Gus

1
太好了,现在我必须再次去做:)
BobTurbo 2011年

7

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

  1. 一个简单的跳转表(如果案件安排在很长的范围内)
  2. 如果有很多空白,则为稀疏(两级)跳转表
  3. 一系列if,如果案例数少或值不接近
  4. 如果情况代表几组间隔较近的范围,则使用上述方法的组合。

简而言之,如果该开关看起来比一系列ifs慢,则编译器可能会将其转换为一个。而且可能不仅是每种情况的比较序列,而且是二叉搜索树。请参阅此处的示例。


实际上,编译器还可以将其替换为散列和跳转,这比您建议的稀疏两级解决方案要好。
爱丽丝

5

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


+1感谢您对#2的回答!:)(顺便说一句,是更有可能的结果。)
user541686


4

以下是旧的(现在很难找到)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向测试的时间的1.6倍-但是即使具有稀疏值,仍然比10维if/ 的速度快两倍else if

底线:只使用了4路测试将不能真正告诉你很多关于性能switchVS if/ else。如果从这个代码从数字上看,这是很容易插了一个事实,一个4路测试,我们预计这两个产生非常相似的结果(〜2.8纳秒为if/ else,〜2.0 switch)。


1
如果我们不知道测试是否故意寻找与if/ else链末尾不匹配或仅匹配的值,或者分散它们,等等,就很难知道该怎么做。bench++在10点后找不到源分钟谷歌搜索。
Tony Delroy 2014年

3

请注意,当未将开关编译到跳转表时,您通常可以编写if,其效率要比开关高。

(1)如果案例是有序的,而不是针对所有N进行最坏情况的测试,则可以编写if来测试是在上半部分还是下半部分中进行测试,然后在每半部分中使用二进制搜索样式...导致最坏的情况是logN而不是N

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


这显然是不正确的。编译器不仅能够同时进行这些优化。
爱丽丝

1
爱丽丝,在您预期的工作负载中,编译器应如何知道哪些情况比其他情况更常见?(A:它不可能知道,所以它不可能做这样的优化。)
Brian Kennedy

(1)可以轻松完成,并且在某些编译器中只需简单地执行二进制搜索即可完成。(2)可以通过多种方式进行预测,或指示给编译器。您是否从未使用过GCC的“可能”或“不太可能”?
爱丽丝

一些编译器允许以某种方式运行程序,该方式可以收集统计信息,然后根据该信息进行优化。
Phil1970年

2

不,如果跳转,则跳转;否则,则跳转...跳转表将具有地址表或使用哈希或类似的内容。

快慢是主观的。例如,您可能将案例1放在最后而不是放在第一位,并且如果您的测试程序或真实程序在大多数情况下都使用案例1,则此实现会使代码变慢。因此,仅根据实施情况重新排列案例列表,就可以带来很大的不同。

如果您使用的是0-3而不是1-4,那么编译器可能已经使用了跳转表,那么编译器应该已经想出了删除+1的方法。也许是少数项目。例如,如果您将其设置为0-15或0-31,则可能是通过表实现的,或者使用了其他快捷方式。只要编译器满足源代码的功能,它就可以自由选择实现方式。这涉及到编译器差异,版本差异和优化差异。如果要跳转表,请创建一个跳转表,如果要使用if-then-else树,请创建一个if-then-else树。如果要由编译器决定,请使用switch / case语句。


2

不过,不知道为什么一个更快,一个更快。

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

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

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

很难预测一段代码在现代编译器和CPU中的执行情况,并且每一代都会变得越来越困难。最好的建议是“甚至不要费劲尝试;始终进行配置”。该建议会越来越好-可以成功忽略它的人群每年都会减少。

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


2
我不知道减速的速度可能是数百倍。分支预测错误的最坏情况是管道停顿,在大多数现代CPU上,它的运行速度将降低约20倍。没有数百次。(好吧,如果您使用的是旧的NetBurst芯片,速度可能会慢35倍...)
Billy ONeal

@Billy:好的,所以我有点期待。 在Sandy Bridge处理器上,“每个错误预测的分支都会刷新整个管道,从而丢失多达100条正在进行的指令的工作”。总的来说,管道的确确实与每一代人
Nemo

1
不对。P4(NetBurst)有31个流水线阶段;桑迪桥的舞台明显更少。我认为“丢失100条左右的指令的工作”是在指令缓存无效的假设下进行的。对于一般的间接跳转实际上确实会发生,但是对于诸如跳转表之类的东西,间接跳转的目标很可能位于指令缓存中的某个位置。
比利·奥尼尔

@比利:我不认为我们不同意。我的发言是:“错误预测的分支比正确预测的分支要贵数十到数百倍”。也许有点夸张……但是除了I-cache和执行管道深度方面的问题,还有更多的事情要做;从我所读的内容来看,仅解码队列就是大约20条指令。
Nemo

如果分支预测硬件对执行路径有错误的预测,则只需将指令管道中来自错误路径的指令移到它们所在的位置,而不会停止执行。我不知道这是怎么可能的(或者我是否对它有误解),但是显然在Nehalem中没有带有错误分支预测的管道停顿吗?(然后,我没有i7;我有i5,因此这不适用于我的情况。)
user541686 2011年

1

没有。在大多数特殊情况下,您进入汇编器并进行实际性能评估时,您的问题就是错误的。对于给定的示例,您的想法肯定太短了,因为

counter += (4 - counter % 4);

在我看来,这是您应该使用的正确增量表达式。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.