反汇编(二)C/C++ 探究 ifelse语句跟switch的区别 以及 在有些情况下switch性能更优的原因

本文深入探讨了编译器如何优化if-else和switch语句,揭示了在不同条件下编译器采用的优化策略,包括线性结构的基址加偏移寻址和非线性结构的二叉平衡树搜索,以及这些优化对性能的影响。

这篇文章讲述了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.还有一种这里不做过多讨论,因为个人碰到的都是第二种方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值