自学内容网 自学内容网

跟我学C++中级篇——if条件语句与switch比较

一、背景介绍

在前面的一些分析介绍以及网上的资料或者书籍中,都有说过如果条件分支语句较多时,推荐使用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_startproc
endbr64
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq%rsp, %rbp
.cfi_def_cfa_register 6
subq$16, %rsp
movl%edi, -4(%rbp)
cmpl$1, -4(%rbp)
jne.L2
leaq.LC0(%rip), %rax
movq%rax, %rsi
leaq_ZSt4cout(%rip), %rax
movq%rax, %rdi
call_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
movq%rdx, %rsi
movq%rax, %rdi
call_ZNSolsEPFRSoS_E@PLT
jmp.L9

.L2:
        cmpl    $2, -4(%rbp)
        jne     .L4
        leaq    .LC1(%rip), %rax
        movq    %rax, %rsi
        leaq    _ZSt4cout(%rip), %rax
        movq    %rax, %rdi
        call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
        movq    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
        movq    %rdx, %rsi
        movq    %rax, %rdi
        call    _ZNSolsEPFRSoS_E@PLT
        jmp     .L9
.L4:
        cmpl    $3, -4(%rbp)
        jne     .L5
        leaq    .LC2(%rip), %rax
        movq    %rax, %rsi
        leaq    _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_startproc
endbr64
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq%rsp, %rbp
.cfi_def_cfa_register 6
subq$16, %rsp
movl%edi, -4(%rbp)
cmpl$6, -4(%rbp)
ja.L11
movl-4(%rbp), %eax
leaq0(,%rax,4), %rdx
leaq.L13(%rip), %rax
movl(%rdx,%rax), %eax
cltq
leaq.L13(%rip), %rdx
addq%rdx, %rax
notrack jmp*%rax
.L18:
        leaq    .LC7(%rip), %rax
        movq    %rax, %rsi
        leaq    _ZSt4cout(%rip), %rax
        movq    %rax, %rdi
        call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
        movq    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
        movq    %rdx, %rsi
        movq    %rax, %rdi
        call    _ZNSolsEPFRSoS_E@PLT
        jmp     .L19
.L17:
        leaq    .LC8(%rip), %rax
        movq    %rax, %rsi
        leaq    _ZSt4cout(%rip), %rax
        movq    %rax, %rdi
        call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
        movq    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
        movq    %rdx, %rsi
        movq    %rax, %rdi
        call    _ZNSolsEPFRSoS_E@PLT
        jmp     .L19

从上面的汇编代码中可以发现,在if分支处理中,每次都要进行比较cmpl动作,而在switch中则只在开始进行了比较然后就是一个notrack jmp。其实编译器比想象的这些还要复杂,比如值得连续性到何种程度使用何种形式的表,多么的分散算可以使用二分查找。
一般来说,Case间隔值大于256时,可能会生成一种类似树的结构,使用二分查找;如果是连续的值,则会生成一个大的散列表,然后直接跳转;如果是连续的值中有一些间隔,可能是使用一个大的散列表再加一个小的散列表的方法,防止内存浪费,直接跃过一些无效的Case;而在很少的情况下,等同于if分支语句。
这些都可以自行测试一下。不过,不同的编译器可能有所不同。

三、整体的分析

可以从两咱形式上考虑问题:
1、形式上考虑,主要是对使用二者在上层开发者的开发习惯、维护等考虑,这种情况下一般对效率不是太敏感或者说二者的效率没有特别的差距:
if条件分支语句更接近于人类的普通认知,所以一般三个以下的分支判断建议还是用if来处理。这样更容易维护,而且也不是所有的场景下都需要快速的跳转,所以此时就看整体的开发者的控制了。而在五个及以上时,建议使用switch,一个是更容易优化,另外也容易处理一些通用逻辑。当然,switch太多也让人心烦。在某些场景下见过上千个以上的Case,这还是相当要命的。另外,如果if条件分支语句可以避免嵌套if分支时,也建议使用switch,比如用switch来处理多个条件使用相同的处理逻辑时,如果用if可能会进行嵌套处理,就不如前者容易维护和理解。
2、技术上考虑
如果开启了编译器优化,则开发者对此几乎是无法控制了。编译器会根据自己的喜好来处理最终的编译结果。一般情况下,实际开发者很难遇到十个以上的条件判断并同时需要效率的强需求情况。象网上那种几百个条件分支和判断再嵌套多少层的。这就不是单纯的技术问题了,这是设计有问题。所以,除了在一些效率要求特别强(比如航天或者说实时系统等)的场景下,大家还是根据自己的情况处理即可,不用刻意控制。

四、总结

细节的重要程度看开发者所处的环境。实际的需求才是对细节把握程度的要求点,开发者不必为每一个细节焦虑,这个没有意义。要学会抓大放小,能收能放,才能更好的解决实际问题。还是那句话,要重视细节但不要陷入细节。


原文地址:https://blog.csdn.net/fpcc/article/details/142603339

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!