C++性能剖析教程之switch语句

前言

几乎每本面向初学者的C语言或C++书籍在前面两章都会提到分支控制语句if……else和switch……case,在某些情况下这两种分支控制语句可以互相替换,但却很少有人去深究在if……else和switch……case语句的背后到底有什么异同?应该选择哪一个语句才能使得效率最高?要回答这些问题,只能走到switch语句的背后,看看这些语句到底是怎么实现的。

基本格式

switch语句的基本格式如下:

switch (表达式) {
case 常量表达式1:《语句序列1》《break;》 //《》中的内容可省
……
case 常量表达式n:《语句序列n》《break;》 //同上,下同
《default:语句序列》
}

其中:

  • 表达式——称为“条件表达式”,用作判断条件,取值为整型、字符型、布尔型或枚举型。
  • 常量表达式——由常量构成,取值类型与条件表达式相同。
  • 语句序列——可以是一个语句也可以是一组语句。

if语句与switch语句

相信学过C/C++的同学对这两个语句的异同早就了如指掌,if语句作为条件判断,满足条件进入if语句块,不满足条件则进入else语句块,而且if和else语句块又可以继续嵌套if语句。switch则是通过判断一个整型表达式的值来决定进入到哪一个case语句中,如果所有case条件都不满足则进入到default语句块。

//简单的if语句
if (a == 1)
 i = 1;
else if (a == 2)
 i = 2;
else
 i = 3;
//简单的switch语句
switch (a){
 case 1: i = 1;
 case 2: i = 2;
 default: i = 3;
}

编译器如何实现switch语句?

现在编译器已经足够智能和强大,经过测试,g++实现switch语句的方式就至少有三种,编译器会根据代码的实际情况,权衡时间效率和空间效率,去选择一个对当前代码而言综合效率最高的一种。

编译器实现switch语句的三种方式:

  • 逐条件判断
  • 跳转表
  • 二分查找法

后面我们将就这三种实现方法适用的代码场景进行测试和分析。

1. 逐条件判断法

逐条件判断法其实就是和if……else语句的汇编实现相同,编译器把switch语句中各个case条件逐个进行判断,直到找到正确的case语句块。这种方法适用于switch语句中case条件很少的情况,即使逐个条件判断也不会导致大量时间和空间的浪费,比如下面这段代码:

#include <algorithm>
int test_switch(){
 int i ;
 int a = std::rand();
 switch(a){
  case 0: i = 0;break;
  case 1: i = 1;break;
  case 2: i = 2;break;
  default: i = 3;break;
 }
 return i;
}

该代码对应的汇编代码如下:

 movl -4(%rbp), %eax
 cmpl $1, %eax
 je .L3
 cmpl $2, %eax
 je .L4
 testl %eax, %eax
 jne .L8
 movl $0, -8(%rbp)
 jmp .L6
.L3:
 movl $1, -8(%rbp)
 jmp .L6
.L4:
 movl $2, -8(%rbp)
 jmp .L6
.L8:
 movl $3, -8(%rbp)
 nop

eax寄存器存储的是判断条件值(对应于C++代码中的a值),首先判断a是否等于1,如果等于1则跳转到.L3执行a==1对应的代码段,然后判断a是否等于2,如果等于2则跳转到.L4执行a==2对应的代码段……可能难理解的是第6行代码testl %eax, %eax,其实这只是编译器提高判断一个寄存器是否为0效率的一个小技巧,如果eax不等于0则跳转到.L8代码段,执行default代码段对应的代码,如果eax等于0则执行a==0对应的代码段。

由上面对编译器生成汇编代码的分析,我们可以发现:编译器在这种情况下使用逐个条件判断来实现switch语句。

2. 跳转表实现法

在编译器采用这种switch语句实现方式的时候,会在程序中生成一个跳转表,跳转表存放各个case语句指令块的地址,程序运行时,首先判断switch条件的值,然后把该条件值作为跳转表的偏移量去找到对应case语句的指令地址,然后执行。这种方法适用于case条件较多,但是case的值比较连续的情况,使用这种方法可以提高时间效率且不会显著降低空间效率,比如下面这段代码编译器就会采用跳转表这种实现方式:

#include <algorithm>

int test_switch(){
 int i ;
 int a = std::rand();
 switch(a){
  case 0: i = 0;break;
  case 1: i = 1;break;
  case 2: i = 2;break;
  case 3: i = 3;break;
  case 4: i = 4;break;
  case 5: i = 5;break;
  case 6: i = 6;break;
  case 7: i = 7;break;
  case 8: i = 8;break;
  case 9: i = 9;break;
  default: i = 10;break;
 }
 return i;
}

该代码对应的汇编代码如下:

 movl -4(%rbp), %eax
 movq .L4(,%rax,8), %rax
 jmp *%rax
.L4:
 .quad .L3
 .quad .L5
 .quad .L6
 .quad .L7
 .quad .L8
 .quad .L9
 .quad .L10
 .quad .L11
 .quad .L12
 .quad .L13
 .text
.L3:
 movl $0, -8(%rbp)
 jmp .L14
.L5:
 movl $1, -8(%rbp)
 jmp .L14
#后面省略……

在x64架构中,eax寄存器是rax寄存器的低32位,此处我们可以认为两者值相等,代码第一行是把判断条件(对应于C++代码中的a值)复制到eax寄存器中,第二行代码是把.L4段偏移rax寄存器值大小的地址赋值给rax寄存器,第三行代码则是取出rax中存放的地址并且跳转到该地址处。我们可以清楚的看到.L4代码段就是编译器为switch语句生成的存放于.text段的跳转表,每种case均对应于跳转表中一个地址值,我们通过判断条件的值即可计算出来其对应代码段地址存放的地址相对于.L4的偏移,从而实现高效的跳转。

3. 二分查找法

如果case值较多且分布极其离散的话,如果采用逐条件判断的话,时间效率会很低,如果采用跳转表方法的话,跳转表占用的空间就会很大,前两种方法均会导致程序效率低。在这种情况下,编译器就会采用二分查找法实现switch语句,程序编译时,编译器先将所有case值排序后按照二分查找顺序写入汇编代码,在程序执行时则采二分查找的方法在各个case值中查找条件值,如果查找到则执行对应的case语句,如果最终没有查找到则执行default语句。对于如下C++代码编译器就会采用这种二分查找法实现switch语句:

#include <algorithm>

int test_switch(){
  int i ;
  int a = std::rand();
  switch(a){
    case 4: i = 4;break;
    case 10: i = 10;break;
    case 50: i = 50;break;
    case 100: i = 100;break;
    case 200: i = 200;break;
    case 500: i = 500;break;
    default: i = 0;break;
  }
  return i;
}

改代码段对应的汇编代码为:

    movl -4(%rbp), %eax
 cmpl $50, %eax
 je .L3
 cmpl $50, %eax
 jg .L4
 cmpl $4, %eax
 je .L5
 cmpl $10, %eax
 je .L6
 jmp .L2
.L4:
 cmpl $200, %eax
 je .L7
 cmpl $500, %eax
 je .L8
 cmpl $100, %eax
 je .L9
 jmp .L2

代码第二行条件值首先与50比较,为什么是50而不是放在最前面的4?这是因为二分查找首先查找的是处于中间的值,所以这里先与50进行比较,如果eax等于50,则执行case

50对应代码,如果eax值大于50则跳转到.L4代码段,如果eax小于50则继续跟4比较……直至找到条件值或者查找完毕条件值不存在。可以看出二分查找法在保持了较高的查询效率的同时又节省了空间占用。

总结

何时应该使用if……else语句,何时应该使用switch……case语句?

通过上面的分析我们可以得出结论,在可能条件比较少的时候使用if……else和switch……case所对应的汇编代码是相同的,所以两者在性能上是没有区别的,使用哪一种取决于个人习惯。如果条件较多的话,显而易见switch……case的效率更高,无论是跳转表还是二分查找都比if……else的顺序查找效率更高,所以在这种情况下尽量选用switch语句来实现分支语句。当然如果我们知道哪种条件出现的概率最高,我们可以将这个条件放在if判断的第一个,使顺序查找提前结束,这时使用if……else语句也可以达到较高的运行效率。

switch语句也有他本身的局限性,即switch语句的值只能为整型,比如当我们需要对一个double型数据进行判断时,便无法使用switch语句,这时只能使用if……else语句来实现。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 解析c语言switch中break语句的具体作用

    问题:break在for循环.while循环等循环流程控制中起的作用是停止执行break后面的语句,跳出本次循环,并跳出该循环控制体:在switch条件选择中,没有了循环控制,break又起什么作用呢? 解决办法:1. switch语句的执行流程是:首先计算switch后面圆括号中表达式的值,然后用此值依次与各个case的常量表达式比较,若圆括号中表达式的值与某个case后面的常量表达式的值相等,就执行此case后面的语句,执行后遇到break语句就退出switch语句,程序流程转向开关语句的下

  • 详解C语言中条件判断语句if和switch的用法

    if 语句 用 if 语句可以构成分支结构,它根据给的条件进行判定,以决定执行哪个分支程序段. C 语言的 if 语句有三种基本形式 第一种形式: if(条件表达式) { 语句1: } if(条件表达式) { 语句1: } 这种形式运行顺序为:当条件表达式为真,执行语句1,否则,直接跳过语句1,执行后面的语句. 例子1: BOOL result = YES: if(result) { printf("result is true\n"); } BOOL result = YES: if

  • C语言switch 语句的用法详解

    C语言虽然没有限制 if else 能够处理的分支数量,但当分支过多时,用 if else 处理会不太方便,而且容易出现 if else 配对出错的情况.例如,输入一个整数,输出该整数对应的星期几的英文表示: #include <stdio.h> int main(){ int a; printf("Input integer number:"); scanf("%d",&a); if(a==1){ printf("Monday\n&q

  • C++性能剖析教程之switch语句

    前言 几乎每本面向初学者的C语言或C++书籍在前面两章都会提到分支控制语句if--else和switch--case,在某些情况下这两种分支控制语句可以互相替换,但却很少有人去深究在if--else和switch--case语句的背后到底有什么异同?应该选择哪一个语句才能使得效率最高?要回答这些问题,只能走到switch语句的背后,看看这些语句到底是怎么实现的. 基本格式 switch语句的基本格式如下: switch (表达式) { case 常量表达式1:<语句序列1><break;

  • Javascript基础教程之switch语句

    stwith语句的格式一般如下: 复制代码 代码如下: switch (expression){      case value :statement1          break;      case value2 :statement2          break;      ....          case value: statement          break;      default :statement; 每个情况表示如果expression的值等于case ,则执

  • MySQL基础教程之DML语句详解

    目录 DML 语句 1.插入记录 2.更新记录 3.简单查询记录 4.删除记录 5.查询记录详解(DQL语句) 5.1.查询不重复的记录 5.2.条件查询 5.3.聚合查询 5.4.排序查询 5.5.limit查询 5.6.连表查询 5.7.子查询 5.8.记录联合 5.9.select语句的执行顺序 6.总结 DML 语句 DML(Data Manipulation Language)语句:数据操纵语句. 用途:用于添加.修改.删除和查询数据库记录,并检查数据完整性. 常用关键字:insert

  • MySQL实战教程之Join语句执行流程

    目录 Join语句执行流程 一.Index Nested-Loop Join 二.Simple Nested-Loop Join 三.Block Nested-Loop Join 四.总结 Join语句执行流程 Hi,我是阿昌,今天学习记录的是关于Join语句执行流程的内容. 在实际生产中,关于 join 语句使用的问题,一般会集中在以下两类: 不让使用 join,使用 join 有什么问题呢? 如果有两个大小不同的表做 join,应该用哪个表做驱动表呢? 创建两个表 t1 和 t2 来说明.

  • Python入门教程之if语句的用法

    Python中的if语句是类似的其它语言的. if语句包含使用该数据进行比较,并根据比较的结果做出了决定的逻辑表达式. 语法: if语句在Python编程语言的语法是: if expression: statement(s) 如果布尔表达式的计算结果为true,那么if语句块将被执行.如果if语句布尔表达式计算为false,那么第一组代码将被执行. Python编程语言的假定任何非零和非null为true,如果是zero或null,则假定为false值. 例子: #!/usr/bin/pytho

  • Javascript基础教程之while语句

    循环语句的作用是反复的执行同一段代码,尽管分几种不同的类型,但其原理几乎相同:只要给定的条件满足,包含在循环体内的语句会不断执行,一旦条件不再满足则终止. while循环是前测试循环,这意味着是否终止的条件判断是在执行代码之前,因此,循环的主体可能根本不执行.其语法如下: while(expression) statement 当expression为ture时,程序会不断执行statement语句,直到expression为false时. 两个案例 复制代码 代码如下: <script typ

  • android开发教程之switch控件使用示例

    复制代码 代码如下: <Switchandroid:id="@+id/open"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textOff="蓝牙关闭中"android:textOn="蓝牙开启中" /> 复制代码 代码如下: open.setOnCheckedChangeListe

  • 深入剖析Go语言编程中switch语句的使用

    switch语句可以让一个变量对反对值的列表平等进行测试.每个值被称为一个的情况(case),变量被接通检查每个开关盒(switch case). 在Go编程,switch有两种类型. 表达式Switch - 在表达式switch,case包含相比较,switch表达式的值. 类型Switch - 在这类型switch,此时含有进行比较特殊注明开关表达式的类型. 表达式Switch 在Go编程语言中表达switch语句的语法如下: 复制代码 代码如下: switch(boolean-expres

  • PHP内核学习教程之php opcode内核实现

    opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定. 除了指令本身以外通常还有指令所需要的操作数,可能有的指令不需要显式的操作数. 这些操作数可能是寄存器中的值,堆栈中的值,某块内存的值或者IO端口中的值等等. 通常opcode还有另一种称谓:字节码(byte codes). 例如Java虚拟机(JVM),.NET的通用中间语言(CIL: Common Intermeditate Language)等等. 1. Opcode简介 opcode是计算

  • Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket

    Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket Kotlin提供了一些机制来扩展已有的类,如下: 还记得我们之前写过的Point3D类吗?(将其略作修改,将成员变量改为Double类型) 让我们为其扩展一个length函数 扩展的方法很简单,只要在函数名前面加上类名就行了. 这样Point3D的对象就有了一个名为length的方法. 运行的结果不出所料: 除此之外,在Kotlin中还有一些特殊的类,比如Data Class: 有些类只包含数据,

随机推荐