switch体内部优化运行原理

      我前端时间曾经写过一篇博客,关于如何提高switch运行效率的。碰巧的是,昨天在知乎上,看到R大关注的一个问题:(

如果穿越成1972年的Dennis Ritchie,你会怎样重新设计C语言?

),当时我以为这个问题正好撞我“枪口”上,所以斗胆回答了一发:

(如果我拥有C之父的示例,我想我会在switch上做手脚,做一个switch的姐妹版,比如叫【gotoswitch】,用它来专门替换多循环、多分支情况下的switch。

大概原理这样:当编译器发现程序中有gotoswitch时,会在全局区开辟出一段数组空间,比如叫【go_code_add】,用它来存放gotoswitch体中的各种跳转地址,这个数组中的所有元素,默认初始化为【go_default】的指令地址。

举例说明,机器进入【gotoswitch】体中编译时,遇到了语句【go_case 7:】,那么编译器先把紧挨着语句【go_case 7:】的下一条指令地址提取出来,赋值给数组首地址往后偏移7个位置的元素中。这样的话,程序在执行到gotoswitch时,如果gotoswitch圆括号内表达式所产生的值是7,那么就直接跳转到【 go_code_add[7] 】这个元素所保存的指令地址中。这样的话,我想多少都会提高运行时效率吧),

回答后没过多长时间,有幸得到轮子哥的点评:(这个优化早就有了),这才一语惊醒梦中人,难道我以前关于switch体的认知都是自欺欺人?人家真正的面貌不知道比我想象的要高到哪里去了??于是我就在vc6上做了一次简单的小测验,结果证明前辈们确实高出我很多很多,所以写成这篇博客,算是巩固记忆吧。以下正文开始:

      先上测验用的源代码:

#include<iostream>
using namespace std;
void main(void)
{
	int a;
	cin >> a;
	switch (a)
	{

	case 3:
         cout << 3;
		 break;	
	case 5:
         cout << 5;
		 break;		 	
	case 8:
		 cout << 8;
		 break;
	case 15:
		 cout << 15;
		 break;		 
	}

}

然后我们看一下编译过的汇编截图:

从switch(a)这个部分开始,先看头两句汇编:

其中的[ebp-4],代表的是变量a的地址,这三句汇编的作用是,把变量a的值赋值给寄存器ecx和edx,是不是这样,我们看一下寄存器的值就知道了,其中的ecx和edx,果真都变成了5;

然后再看下一句汇编:(sub edx 3),在这里为什么要给edx减去3呢?这是因为在switch体中,编译器首先要生成一个字节数组,字节数组的长度取决于分支中最大的常量值减去分支中最小的常量值,所以本例中是12,而且又因为数组首下标得是0 的缘故,所以这里必须要进行一次裁段对齐的操作!如果你把本例中的最小常量值改成5,那么就会变成(sub edx 5)。除此之外,编译器还要生成一个整型数组,用来记录各种指令跳转地址,整型数组的长度取决于switch体中有多少分支(还要+1),本例中分支有4个,所以整型数组的长度是5。

我们再接着往下看,下三句的汇编意思是,先把edx保存在[ebp-8]的位置中,然后拿他出来跟常量0CH(本来应该是0FH,因为减3的缘故,这里也要变成0CH)比较,看看[ebp-8]位置保存的数值是不是在0到12的范围内(如果我们给a的值是1,那么1-3以后会变成负数,而cmp与ja搭配,进行的是无符号类型比较,所以-2换成无符号类型,一定是一个很大的正整数,所以自然会大于0CH,这样也就相当于比较数值在不在0到12的集合范围内了......),如果不在0~12范围内,那么接下来程序直接跳转到switch体外继续执行。


最后的最后,我们的重点来了!在开始讲解之前,我们看看这两个鬼:


请注意图中两个黑框之中的内容,其中第一个(4018a8)表示的是整型数组的首地址,也就是保存跳转指令地址的那个数组的首地址;而在它下面的(401894),是字节数组的首地址。让我们先到字节数组的首地址(4018a8)去看看:


我说过,本例中我们的整型数组长度是5,所以在字节数组中,数值的取值范围也只能是从0到4,既0、1、2、3、4,仔细看图,4出现的次数最多,这是因为使用它可以到达整型数组的最后一个指令地址,也就是switch的default指令地址。至于其它的0、1、2、3,它们没人只需出现一次即可。好了,接下来由我来带领您进入机器的内部执行这些汇编指令吧:

前两句汇编不要管了,看箭头指的是地方,它表示以(4018A8)为首地址,往后偏移(ecx)个位置,其中ecx的值是2,所以总意思就是:取出4018AA(4018A8+2)地址里面的值,赋值给AL,既然这样,那就让我们跳转到4018AA这个地址,看看里面保存的是什么值:

图中红色箭头的位置就是AA,我们看到它里面的数值是1;

那么对不对呢,我们接下来再看:


看到没有,里面eax就是我们所要保存的位置,里面的元素值也确实是1;那么继续往下,看平箭头指向的语句,它表示程序立刻无条件跳转到(eax*4+401894h)地址所保存的数值(一个指令地址)中,因为eax=1,所以括号内表达式产生的值就是401898,好了,让我们到那里看个究竟吧:

我们看到,在dword ptr[00401898]位置中,所保存的是数值(0040185B),也就是说,程序要跳转到这个指令地址继续执行!【我在调试这个程序的时候,输入a的数值是5,先前没有截图说明,抱歉!】。那么究竟对不对呢,让我们看看在(case 5:)下的第一条指令地址就可以验证了:

验证结果是,一切OK!

      最后有必要说下,因为我们看到编译器生成的数组有一个是字节数组,那么如果我们的switch分支中有的常量值大于256怎么办?我其实也做了相关测验,发现这时编译器会采取另一种模式编译,大概就是类似二叉树那样的吧,我这里就不多说了,感兴趣的朋友可以自己测验下,谢谢阅读!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值