一、背景介绍
在前面的一些分析介绍以及网上的资料或者书籍中,都有说过如果条件分支语句较多时,推荐使用switch而不要使用if语句,因为前者的速度比后者要快,特别是数量多时,会比较明显。但其实这句话是有问题的。在经常写类似条件语句的开发者眼中看来,可能很少考虑二者到底有什么不同,心中有一个大概的印象就可以了。真正开发时,很少有这种较真的场景。
答案确实也是如此。但正因为这样,大多数的开发者往往忽略这个问题或者觉得它不值一哂。但学习的过程不能放过每一个细节,可以不对其重视,但不能不知道其所以然。
二、if和switch的处理
一般来说,传统的认知中可能是下面的想法:
1、if会对条件进行反复不断的比较来最终确定跳转的分支语句
2、switch则会是查一个表来进行跳转,所以它的效率会高
但事实一定是这样么?
整体上来言,上面的下意识的想法不能说错误,只能说不完全正确。但是实际上编译器的处理还是有一定的复杂性的:
1、编译器优化的情况下,二者没有差别的。所以,在前面反复提到过不要小看编译器的进步
2、即使是在没有开优化的前提下,如果值的内容有所不同,也有可能产生一些意想不到的情况。比如值比较分散,则编译器对switch使用二分查找方式,如果比较连续,则使用无冲突的散列表来进行处理。
3、在非常少的比较时,二者几乎也没有什么差距,比如只有两个分支
看一下这个例子:
#include <iostream>using namespace std;
void testIf(int d) {if (d == 1) {std::cout << "call 1 !" << std::endl;} else if (d == 2) {std::cout << "call 2 !" << std::endl;} else if (d == 3) {std::cout << "call 3 !" << std::endl;} else if (d == 4) {std::cout << "call 4 !" << std::endl;} else if (d == 5) {std::cout << "call 5 !" << std::endl;} else if (d == 6) {std::cout << "call 6 !" << std::endl;} else {std::cout << "call default !" << std::endl;}
}
void testSwitch(int d) {switch (d) {case 1:std::cout << "call 1 branch!" << std::endl;break;case 2:std::cout << "call 2 branch!" << std::endl;break;case 3:std::cout << "call 3 branch!" << std::endl;break;case 4:std::cout << "call 4 branch!" << std::endl;break;case 5:std::cout << "call 5 branch!" << std::endl;break;case 6:std::cout << "call 6 branch!" << std::endl;break;default:std::cout << "call default branch!" << std::endl;break;}
}
int main() {int d = 3;testSwitch(d);testIf(d);return 0;
}
它的反汇编代码可以看出不同来:
//testIF:
.LC0:.string "call 1 !"
.LC1:.string "call 2 !"
.LC2:.string "call 3 !"
.LC3:.string "call 4 !"
.LC4:.string "call 5 !"
.LC5:.string "call 6 !"_Z6testIfi:
.LFB1731:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl %edi, -4(%rbp)cmpl $1, -4(%rbp)jne .L2leaq .LC0(%rip), %raxmovq %rax, %rsileaq _ZSt4cout(%rip), %raxmovq %rax, %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdxmovq %rdx, %rsimovq %rax, %rdicall _ZNSolsEPFRSoS_E@PLTjmp .L9.L2:cmpl $2, -4(%rbp)jne .L4leaq .LC1(%rip), %raxmovq %rax, %rsileaq _ZSt4cout(%rip), %raxmovq %rax, %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdxmovq %rdx, %rsimovq %rax, %rdicall _ZNSolsEPFRSoS_E@PLTjmp .L9
.L4:cmpl $3, -4(%rbp)jne .L5leaq .LC2(%rip), %raxmovq %rax, %rsileaq _ZSt4cout(%rip), %rax
//testSwitch:
.LC7:.string "call 1 branch!"
.LC8:.string "call 2 branch!"
.LC9:.string "call 3 branch!"
.LC10:.string "call 4 branch!"
.LC11:.string "call 5 branch!"
_Z10testSwitchi:
.LFB1732:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl %edi, -4(%rbp)cmpl $6, -4(%rbp)ja .L11movl -4(%rbp), %eaxleaq 0(,%rax,4), %rdxleaq .L13(%rip), %raxmovl (%rdx,%rax), %eaxcltqleaq .L13(%rip), %rdxaddq %rdx, %raxnotrack jmp *%rax
.L18:leaq .LC7(%rip), %raxmovq %rax, %rsileaq _ZSt4cout(%rip), %raxmovq %rax, %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdxmovq %rdx, %rsimovq %rax, %rdicall _ZNSolsEPFRSoS_E@PLTjmp .L19
.L17:leaq .LC8(%rip), %raxmovq %rax, %rsileaq _ZSt4cout(%rip), %raxmovq %rax, %rdicall _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLTmovq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdxmovq %rdx, %rsimovq %rax, %rdicall _ZNSolsEPFRSoS_E@PLTjmp .L19
从上面的汇编代码中可以发现,在if分支处理中,每次都要进行比较cmpl动作,而在switch中则只在开始进行了比较然后就是一个notrack jmp。其实编译器比想象的这些还要复杂,比如值得连续性到何种程度使用何种形式的表,多么的分散算可以使用二分查找。
一般来说,Case间隔值大于256时,可能会生成一种类似树的结构,使用二分查找;如果是连续的值,则会生成一个大的散列表,然后直接跳转;如果是连续的值中有一些间隔,可能是使用一个大的散列表再加一个小的散列表的方法,防止内存浪费,直接跃过一些无效的Case;而在很少的情况下,等同于if分支语句。
这些都可以自行测试一下。不过,不同的编译器可能有所不同。
三、整体的分析
可以从两咱形式上考虑问题:
1、形式上考虑,主要是对使用二者在上层开发者的开发习惯、维护等考虑,这种情况下一般对效率不是太敏感或者说二者的效率没有特别的差距:
if条件分支语句更接近于人类的普通认知,所以一般三个以下的分支判断建议还是用if来处理。这样更容易维护,而且也不是所有的场景下都需要快速的跳转,所以此时就看整体的开发者的控制了。而在五个及以上时,建议使用switch,一个是更容易优化,另外也容易处理一些通用逻辑。当然,switch太多也让人心烦。在某些场景下见过上千个以上的Case,这还是相当要命的。另外,如果if条件分支语句可以避免嵌套if分支时,也建议使用switch,比如用switch来处理多个条件使用相同的处理逻辑时,如果用if可能会进行嵌套处理,就不如前者容易维护和理解。
2、技术上考虑
如果开启了编译器优化,则开发者对此几乎是无法控制了。编译器会根据自己的喜好来处理最终的编译结果。一般情况下,实际开发者很难遇到十个以上的条件判断并同时需要效率的强需求情况。象网上那种几百个条件分支和判断再嵌套多少层的。这就不是单纯的技术问题了,这是设计有问题。所以,除了在一些效率要求特别强(比如航天或者说实时系统等)的场景下,大家还是根据自己的情况处理即可,不用刻意控制。
四、总结
细节的重要程度看开发者所处的环境。实际的需求才是对细节把握程度的要求点,开发者不必为每一个细节焦虑,这个没有意义。要学会抓大放小,能收能放,才能更好的解决实际问题。还是那句话,要重视细节但不要陷入细节。