这篇文章讲述了if else语句,switch语句的底层原理。且比较了两者间的区别以及编译器对switch语句的优化。
其实计算机并不认识if,else等语句,这不过是高级语言为了提高开发效率而生成的中间产物。
那么if (XXX > YYY)在汇编的角度去看,计算机做了运算,把XXX与YYY相减(相当于 X + YYY的补码。因为计算机只会做加法),结果会影响标志寄存器(真实存在的硬件。可以百度下,这篇不会过多的介绍。),然后根据标志寄存器的标志位进行指令地址跳转。这里会在下面体现出来。
另外,计算机只会从上到下的执行指令...从这些看到,计算机其实是个很笨重的东西- -
这里列出switch1(),ifelse()两个函数:
编译器私底下做了许多的事情(代码的优化等)。
例如:”这里只给出debug版本的反汇编,Release版本下,编译器会做出优化。比如在switch1()跟ifelse()函数中的条件判断,编译器可能会直接判断出要执行哪条语句。从而使反汇编出来的代码可能只有3-5条。
#include<cstdio>
#include<iostream>
using namespace std;
void switch1()
{
int a = 1;
switch(a)
{
case 1:printf("1");break;
case 2:printf("2");break;
case 3:printf("3");break;
case 4:printf("4");break;
case 5:printf("5");break;
case 6:printf("6");break;
default:printf("Hello,world");break;
}
}
void ifelse()
{
int a = 1;
if(a == 1)printf("1");
else if(a == 2)printf("2");
else if(a == 3)printf("3");
else if(a == 4)printf("4");
else if(a == 5)printf("5");
else if(a == 6)printf("6");
else printf("Hello,world");
}
int main()
{
ifelse();
switch1();
return 0;
}
直接OD,给出代码:
每行开始的前面是指令在内存中的地址
//每行开始的前面:指令在内存中的地址
00401435 PUSH EBP
00401436 MOV EBP,ESP
00401438 SUB ESP,28
//int a = 1;
0040143B MOV DWORD PTR SS:[EBP-C],1
/*文字描述*/
//把a与1比较
00401442 CMP DWORD PTR SS:[EBP-C],1
//cmp会影响标志寄存器的标志位。然后jxx根据标志位进行跳转
//如果a != 1 则跳转到00401456地址处 --刚好是a与2比较的地址处
00401446 JNZ SHORT switch语.00401456
//如果a ==1 则不跳转。继续执行
//把"1"的数据地址入栈
00401448 MOV DWORD PTR SS:[ESP],switch语.00474065
//调用printf
0040144F CALL switch语.00468EF4
//执行printf后,跳转到004014C6处---刚好是return所在的地址处
00401454 JMP SHORT switch语.004014C6
//把a与2比较
00401456 CMP DWORD PTR SS:[EBP-C],2
//如果a != 2 则跳转到00401456地址处 --刚好是a与3比较的地址处
0040145A JNZ SHORT switch语.0040146A
//如果a ==2 则不跳转。继续执行
//把"2"的数据地址入栈
0040145C MOV DWORD PTR SS:[ESP],switch语.00474067
//调用printf
00401463 CALL switch语.00468EF4
//执行printf后 跳转到004014C6处---刚好是return所在的地址处
00401468 JMP SHORT switch语.004014C6
//把a与3比较
0040146A CMP DWORD PTR SS:[EBP-C],3
//如果a != 3 则跳转到00401456地址处 --刚好是a与4比较的地址处
0040146E JNZ SHORT switch语.0040147E
//如果a ==3 则不跳转。继续执行
//把"3"的数据地址入栈
00401470 MOV DWORD PTR SS:[ESP],switch语.00474069
00401477 CALL switch语.00468EF4
//执行printf后 跳转到004014C6处---刚好是return所在的地址处
0040147C JMP SHORT switch语.004014C6
//以下类推
0040147E CMP DWORD PTR SS:[EBP-C],4
00401482 JNZ SHORT switch语.00401492
00401484 MOV DWORD PTR SS:[ESP],switch语.0047406B
0040148B CALL switch语.00468EF4
00401490 JMP SHORT switch语.004014C6
00401492 CMP DWORD PTR SS:[EBP-C],5
00401496 JNZ SHORT switch语.004014A6
00401498 MOV DWORD PTR SS:[ESP],switch语.0047406D
0040149F CALL switch语.00468EF4
004014A4 JMP SHORT switch语.004014C6
004014A6 CMP DWORD PTR SS:[EBP-C],6
004014AA JNZ SHORT switch语.004014BA
004014AC MOV DWORD PTR SS:[ESP],switch语.0047406F
004014B3 CALL switch语.00468EF4
004014B8 JMP SHORT switch语.004014C6
004014BA MOV DWORD PTR SS:[ESP],switch语.00474071 ; ASCII "Hello,world"
004014C1 CALL switch语.00468EF4
004014C6 LEAVE
004014C7 RETN
可以看到,编译器在对if-elseif-else结构的语句,非常直观的给出了指令,前后也是比较了6次(从1 - 6)。
都说switch在一定情况下性能比ifelse高?那么到底高在哪?
再看下编译器对switch做了哪些优化。
004013B0 PUSH EBP
004013B1 MOV EBP,ESP
004013B3 SUB ESP,28
//反观这里,跟ifelse有着天差地别
//int a = 1;
004013B6 MOV DWORD PTR SS:[EBP-C],1
//把a与6比较
004013BD CMP DWORD PTR SS:[EBP-C],6
//如果a大于6,则直接跳转到default或switch某尾
004013C1 JA SHORT switch语.00401426
//如果a <= 6,继续执行
//把a放到EAX寄存器
004013C3 MOV EAX,DWORD PTR SS:[EBP-C]
//以下为重点
//把EAX左移2位,相当于EAX = EAX * 4;(因为一个地址为四个字节)
004013C6 SHL EAX,2
//相当于这样。
// int address[6] = { case 1的地址(一个四字节的地址值),case 2的地址,case 3的地址
// ,case 4的地址,case 5的地址,case 6的地址}
//最终执行语句地址 = case 1的基地址 + Number * 4(偏移地址),这里的number就是case中的值
//也就是说 最终比较次数1次。但是ifelse里有6次。编译器这里合理运用了1-6的线性结构。
004013C9 ADD EAX,switch语.00474080
004013CE MOV EAX,DWORD PTR DS:[EAX]
004013D0 JMP EAX
004013D2 MOV DWORD PTR SS:[ESP],switch语.00474065
004013D9 CALL switch语.00468EF4
004013DE JMP SHORT switch语.00401433
004013E0 MOV DWORD PTR SS:[ESP],switch语.00474067
004013E7 CALL switch语.00468EF4
004013EC JMP SHORT switch语.00401433
004013EE MOV DWORD PTR SS:[ESP],switch语.00474069
004013F5 CALL switch语.00468EF4
004013FA JMP SHORT switch语.00401433
004013FC MOV DWORD PTR SS:[ESP],switch语.0047406B
00401403 CALL switch语.00468EF4
00401408 JMP SHORT switch语.00401433
0040140A MOV DWORD PTR SS:[ESP],switch语.0047406D
00401411 CALL switch语.00468EF4
00401416 JMP SHORT switch语.00401433
00401418 MOV DWORD PTR SS:[ESP],switch语.0047406F
0040141F CALL switch语.00468EF4
00401424 JMP SHORT switch语.00401433
00401426 MOV DWORD PTR SS:[ESP],switch语.00474071 ; ASCII "Hello,world"
0040142D CALL switch语.00468EF4
00401432 NOP
00401433 LEAVE
00401434 RETN
首先,编译器不会对ifelse做优化(debug版本下),那么我们现在只讨论switch。
那么前面只是针对一个特例--也就是case 1- case 6,刚好是线性结构。刚好可以拼成基地址+偏移地址的情况
那么如果不是线性情况呢?比如像这样的情况呢?:
编译器还会怎么优化?
void switch1()
{
int a = 1;
switch(a)
{
case 1:printf("1");break;
case 5:printf("5");break;
case 14:printf("14");break;
case 58:printf("58");break;
case 69:printf("69");break;
case 255:printf("255");break;
default:printf("Hello,world");break;
}
}
这里编译器对它做出的优化是---二叉平衡树(一种数据结构).
最后会给出图:
004013B0 PUSH EBP
004013B1 MOV EBP,ESP
004013B3 SUB ESP,28
//下面开始只给出重要部分解释
//重要的地方隔开
004013B6 MOV DWORD PTR SS:[EBP-C],1
004013BD MOV EAX,DWORD PTR SS:[EBP-C]
//这里编译器其实做了优化,很难分辨,编译器其实用了二叉平衡树。
//也会采用二分查找,具体看编译器如何优化。
004013C0 CMP EAX,0E
004013C3 JE SHORT switch语.00401405
004013C5 CMP EAX,0E
004013C8 JG SHORT switch语.004013D6
004013CA CMP EAX,1
004013CD JE SHORT switch语.004013E9
004013CF CMP EAX,5
004013D2 JE SHORT switch语.004013F7
004013D4 JMP SHORT switch语.0040143D
004013D6 CMP EAX,45
004013D9 JE SHORT switch语.00401421
004013DB CMP EAX,0FF
004013E0 JE SHORT switch语.0040142F
004013E2 CMP EAX,3A
004013E5 JE SHORT switch语.00401413
004013E7 JMP SHORT switch语.0040143D
004013E9 MOV DWORD PTR SS:[ESP],switch语.00474065
004013F0 CALL switch语.00468F14
004013F5 JMP SHORT switch语.0040144A
004013F7 MOV DWORD PTR SS:[ESP],switch语.00474067
004013FE CALL switch语.00468F14
00401403 JMP SHORT switch语.0040144A
00401405 MOV DWORD PTR SS:[ESP],switch语.00474069 ; ASCII "14"
0040140C CALL switch语.00468F14
00401411 JMP SHORT switch语.0040144A
00401413 MOV DWORD PTR SS:[ESP],switch语.0047406C ; ASCII "58"
0040141A CALL switch语.00468F14
0040141F JMP SHORT switch语.0040144A
00401421 MOV DWORD PTR SS:[ESP],switch语.0047406F ; ASCII "69"
00401428 CALL switch语.00468F14
0040142D JMP SHORT switch语.0040144A
0040142F MOV DWORD PTR SS:[ESP],switch语.00474072 ; ASCII "255"
00401436 CALL switch语.00468F14
0040143B JMP SHORT switch语.0040144A
0040143D MOV DWORD PTR SS:[ESP],switch语.00474076 ; ASCII "Hello,world"
00401444 CALL switch语.00468F14
00401449 NOP
0040144A LEAVE
0040144B RETN
具体图不予给出,但是大致为这样,编译器会自动降低树的高度。根据实际条件判断变化.。
这里条件比较少。条件多了会更明显。
从这里可以看出,其实编译器私底下帮我们做了很多事情。
当然,如果使用 编译器 最大速度优化代码 的选项,人眼可能都看不懂代码....
总结:
编译器对switch的优化可以总结为3点。
1.如果是有序线性判断,起初会比较一次是不是大于case的最大值,然后则编译器会 以基地址+偏移地址的方式跳转到case首地址。
2.如果不是有序线性,则会以二叉平衡树的方式,快速找出对应的case处
3.还有一种这里不做过多讨论,因为个人碰到的都是第二种方式。