Is 'switch' faster than 'if'?
一个
我在VisualStudio 2010的X64 C++编译器上运行了代码,其中EDCOX1引用2标记:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #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... "); printf("Switch statement: %u ms ", testSwitch()); printf("If statement: %u ms ", testIf()); } |
得到这些结果:
Switch statement: 5261 ms
If statement: 5196 ms
据我所知,
X86或X64中的基本跳转表是什么样子的?
此代码是否使用跳转表?
为什么这个例子中没有性能差异?在任何情况下,是否存在显著的性能差异?
代码的分解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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测试,以确保输入在表中有效。还要注意,它只在输入是连续数字的情况下工作。
如果开关中的分支数非常大,编译器可以对开关的值进行二进制搜索之类的操作,这(在我看来)将是一种更有用的优化,因为它在某些情况下会显著提高性能,与开关一样通用,并且不会导致生成的代码更大。但要看到这一点,您的测试代码需要更多的分支来查看任何差异。
要回答您的特定问题:
Clang生成了一个如下所示的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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个比较指令清晰可见:
1 2 3 4 5 6 7 8 | 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中,基本跳转表是什么样子的?
跳转表是存储指向类似数组结构中标签的指针的内存地址。下面的示例将帮助您了解跳转表的外观
1 2 3 4 | 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.是否存在性能差异显著的情况?
如果您有非常长的if检查序列,那么在这种情况下,使用跳转表可以降低性能命中,但这会带来内存成本。
座右铭:编译器足够聪明处理这种情况:)
编译器可以自由地将switch语句编译为等同于if语句的代码,或者创建跳转表。根据编译器选项中指定的内容,它可能会根据执行速度最快的代码或生成最小的代码来选择一个或另一个,所以最坏的情况是,它的速度与if语句的速度相同
我相信编译器会做最好的选择,并专注于什么使代码最可读。
如果事例数变得非常大,跳转表将比一系列if快得多。但是,如果值之间的步骤非常大,则跳转表可能会变大,编译器可能会选择不生成跳转表。
您如何知道您的计算机在切换测试循环期间没有执行与测试无关的任务,在if测试循环期间执行的任务更少?您的测试结果不显示以下内容:
我的结果是:
我补充说:
1 2 |
最后,这样它就不会优化循环,因为在您的示例中从未使用过计数器,所以编译器为什么要执行循环?立即,即使有了这样一个微观基准,这种转变也总是会取得胜利。
代码的另一个问题是:
1 | switch (counter % 4 + 1) |
在你的开关回路中,与
1 | const size_t c = counter % 4 + 1; |
在你的if循环中。如果你能解决这个问题,那就大不一样了。我相信,将语句放入switch语句会激发编译器将值直接发送到CPU寄存器中,而不是先将其放入堆栈中。因此,这有利于switch语句,而不是平衡测试。
哦,我认为你也应该在测试之间重置计数器。实际上,您可能应该使用某种随机数,而不是+1、+2、+3等,因为它可能会优化其中的一些内容。例如,随机数是指基于当前时间的数字。否则,编译器可以将这两个函数都转换成一个长的数学运算,甚至不需要任何循环。
我对Ryan的代码进行了足够的修改,以确保编译器无法在代码运行之前解决问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | #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... "); printf("Switch statement: %lld ms ", testSwitch()); fflush(stdout); printf("counter: %d ", counter); counter = 0; srand(time(NULL)); printf("If statement: %lld ms ", testIf()); fflush(stdout); printf("counter: %d ", counter); } |
开关:3740如果:3980
(多次尝试的结果相似)
我还将cases/ifs的数量减少到了5个,切换功能仍然获胜。
一个好的优化编译器(如msvc)可以生成:
简而言之,如果开关看起来比一系列IFS慢,编译器可能只是将其转换为一个。它可能不仅仅是每种情况的一个比较序列,而是一个二进制搜索树。请参阅此处以获取示例。
我会回答2)并做一些一般性的评论。2)不,您发布的程序集代码中没有跳转表。跳转表是一个包含跳转目的地的表,以及一个或两个直接从表跳转到索引位置的指令。当有许多可能的交换目的地时,跳转表会更有意义。也许乐观者知道简单的if-else逻辑更快,除非目的地的数量大于某个阈值。用"说出20个可能性"而不是"4"来再次尝试您的示例。
我很感兴趣,并查看了可以对您的示例进行哪些更改,以使它更快地运行switch语句。
如果您得到40个if语句,并添加一个0大小写,那么if块的运行速度将比等效的switch语句慢。我这里有结果:https://www.ideone.com/kzecz。
删除0案例的效果可以在这里看到:https://www.ideone.com/lfnrx。
下面是旧的(现在很难找到)bench++基准测试的一些结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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.0x64),每个
使用switch语句,只要值密集,2路测试和10路测试的速度几乎没有差别。稀疏值10路测试的时间约为密集值10路测试的1.6倍,但即使是稀疏值,仍然比10路
底线:仅使用4路测试并不能真正显示出
请注意,当一个开关没有编译成跳转表时,您可以经常写if比开关更有效…
(1)如果案例有一个顺序,而不是所有n的最坏案例测试,那么你可以编写if's来测试if在上半部分或下半部分,然后在每半部分中,使用二进制搜索样式…导致最坏的情况是logn而不是n
(2)如果某些案例/群体比其他案例频繁得多,那么设计您的国际单项体育联合会,首先隔离这些案例,可以加快平均时间通过
Not sure why one is faster and one is slower, though.
这其实不难解释…如果你记得预测失误的分支比正确预测的分支贵几十到几百倍。
在
在
很难预测一段代码如何使用现代编译器和CPU来执行,每一代代码都会变得更加困难。最好的建议是"不要费心去尝试;总是做个侧面的人"。这种建议会越来越好,而那些能忽视它的人每年都会越来越少。
所有这些都是说,我上面的解释很大程度上是一种猜测。-)
不,这些是if-then-jump-else-if-then-jump-else…一个跳转表将有一个地址表,或者使用哈希或类似的东西。
快或慢是主观的。例如,您可以让case 1是最后一件事,而不是第一件事,如果您的测试程序或现实世界程序使用case 1,那么大多数情况下,代码在这个实现中会变慢。因此,只需重新安排案例列表,根据实现情况,就可以产生很大的不同。
如果您使用的是0-3而不是1-4,那么编译器可能使用了一个跳转表,编译器应该已经计算出删除+1了。可能是因为物品数量太少。例如,如果您将其设置为0-15或0-31,它可能使用了一个表来实现它,或者使用了其他一些快捷方式。只要编译器满足源代码的功能,它就可以自由选择如何实现这些东西。这就涉及到编译器的差异、版本的差异和优化的差异。如果你想要一个跳转表,做一个跳转表,如果你想要一个if-then-else树,做一个if-then-else树。如果要让编译器决定,请使用switch/case语句。
一个也没有。在大多数特定的情况下,当您进入汇编程序并进行实际的性能度量时,您的问题仅仅是一个错误的问题。对于给定的例子来说,你的想法显然太短了,因为
1 | counter += (4 - counter % 4); |
在我看来,这是您应该使用的正确增量表达式。