if和switch效率比较及分析

经常听说switch的效率比if-else要高。可是到底高多少呢?又为什么会高呢?我写了两个简单的测试程序进行了比较。

if-else测试代码

int main(int argc, char** argv)
{
	int rounds = atoi(argv[1]);
	int target = atoi(argv[2]);
	clock_t st=clock();
	for(int i=0; i<rounds; i++)
	{
		if (target == 0) continue;
		else if (target == 1) continue;
		else if (target == 2) continue;
		else if (target == 3) continue;
		else if (target == 4) continue;
		else if (target == 5) continue;
		else if (target == 6) continue;
		else if (target == 7) continue;
		else if (target == 8) continue;
		else if (target == 9) continue;
		else continue;
	}
	clock_t ed = clock();
	printf("use time %dms\n", ed-st);
	return 0;
}

switch测试代码

int main(int argc, char** argv)
{
	int rounds = atoi(argv[1]);
	int target = atoi(argv[2]);
	clock_t st=clock();
	for(int i=0; i<rounds; i++)
	{
		switch (target)
		{
			case 0: continue; break;
			case 1: continue; break;
			case 2: continue; break;
			case 3: continue; break;
			case 4: continue; break;
			case 5: continue; break;
			case 6: continue; break;
			case 7: continue; break;
			case 8: continue; break;
			case 9: continue; break;
			default: continue; break;
		}
	}
	clock_t ed = clock();
	printf("use time %dms\n", ed-st);
	return 0;
}

相信应该都能看懂这代码。我们来看下在rounds=1e9, target=5时的执行时间。

可以看出来if-else花费的时间几乎快到switch的一倍了。

那我们再看一下target=9时的情况

两者的差距非常明显。if-else比之前又慢了近一倍,而switch基本没有变化

那么问题来了,为什么会这样呢?我们看一下if-else的汇编代码

	movl	$0, 44(%esp)				[esp+44] = 0  对应 int i=0; 
	jmp	L2
L14:
	cmpl	$0, 36(%esp)				注:&target = [esp+36] 
	jne	L3								if (target != 0) goto L3;
	jmp	L4								else goto L4;
L3:
	cmpl	$1, 36(%esp)
	jne	L5								if (target != 1) goto L5;
	jmp	L4								else goto L4;
L5:
	cmpl	$2, 36(%esp)
	jne	L6								if (target != 2) goto L6;
	jmp	L4								else goto L4;
略。。。 
L12:
	cmpl	$9, 36(%esp)
	jne	L13								if (target != 2) goto L13;
	jmp	L4								else goto L4;
L13:
	nop									空操作指令 
L4:
	addl	$1, 44(%esp)				[esp+44] = [esp+44] + 1
L2:
	movl	44(%esp), %eax				
	cmpl	40(%esp), %eax				注:&rounds = [esp+40]
	jl	L14								if (i < rounds) goto L14;

代码较长,只截取了for循环部分,并且分支也只截取了开头结尾部分。(本人汇编功底基本为0,不敢保证对语句的解释一定正确,语句后面的解释大家看看就好)

根据汇编代码大概能明白,if-else就是硬生生从头到尾一个个比较。这也解释了为什么当target=9时会比target=5时慢那么多。

那我们再来看看switch的汇编代码。同样只取for循环部分。

	movl	$0, 44(%esp)
	jmp	L2
L16:
	cmpl	$9, 36(%esp)
	ja	L18								if (target > 9) togo L18; 
	movl	36(%esp), %eax				eax = target
	sall	$2, %eax					eax = eax << 2   (eax = eax *4)
	addl	$L5, %eax					eax = eax + L5
	movl	(%eax), %eax				没太看懂 
	jmp	*%eax							goto eax   jmp指令的操作数有前缀*,表明这是一个间接跳转
	.section .rdata,"dr"				完全不懂 
	.align 4							按4字节对齐? 
L5:										个人认为可以把L5当成一个指针数组,数组长度是switch分支的个数,值是每个分支对应语句的地址 	 
	.long	L19							case 0: 
	.long	L19							case 1:
	.long	L19							...
	.long	L19
	.long	L19
	.long	L19
	.long	L19
	.long	L19
	.long	L19							case 8:
	.long	L19							case 9:
	.text
L18:
	nop
	jmp	L15								goto L15 
L19:
	nop									空操作
L15:
	addl	$1, 44(%esp)
L2:
	movl	44(%esp), %eax
	cmpl	40(%esp), %eax
	jl	L16

编译器遇到switch实际上是生成了一张跳转表或者说是一个指针数组,里面记录了每个分支对应执行语句的地址。这里因为代码中每个分支的执行都是一个continue,所以跳转表中的地址都一样。

movl	36(%esp), %eax
sall	$2, %eax
addl	$L5, %eax
movl	(%eax), %eax
jmp	*%eax

switch的命中就是简单的一个计算而已。eax知道了target的值,其实就已经知道了target对应语句在跳转表中的位置。target*4是因为每个指针占4个字节。然后加上跳转表的地址,就可以得到实际的语句地址。然后就是跳转执行。

我们再看个switch的代码和汇编实现,加深理解

#include <stdio.h>
int main()
{
	int x=5;
	switch (x)
	{
		case 5:
			x = 8;
			break;
		case 2:
			printf("%d\n", x+2);
			break;
		case 3:
			printf("x+4 = %d\n", x+4);
			break;
		case 4:
			x = x+10 * 2 + 9;
			break;
		case 1:
			printf ("target!\n");
			x = x+5;
			break;
		case 8:
			x = 0;
			break;
	}
	return 0;
}

为了更清楚的表现出case的顺序与跳转表的顺序无关,我特别换了下case的顺序。我们看下他的汇编。

	.file	"test-switch.cpp"
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "%d\12\0"
LC1:
	.ascii "x+4 = %d\12\0"
LC2:
	.ascii "target!\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	movl	$5, 28(%esp)
	cmpl	$8, 28(%esp)
	ja	L2
	movl	28(%esp), %eax
	sall	$2, %eax
	addl	$L4, %eax
	movl	(%eax), %eax
	jmp	*%eax
	.section .rdata,"dr"
	.align 4
L4:
	.long	L2				case 0:
	.long	L3				case 1:
	.long	L5				case 2:
	.long	L6				case 3:
	.long	L7				case 4:
	.long	L8				case 5:
	.long	L2				case 6:
	.long	L2				case 7:
	.long	L9				case 8:
	.text
L8:
	movl	$8, 28(%esp)		x = 8
	jmp	L2
L5:
	movl	28(%esp), %eax
	addl	$2, %eax
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	jmp	L2
L6:
	movl	28(%esp), %eax
	addl	$4, %eax
	movl	%eax, 4(%esp)
	movl	$LC1, (%esp)
	call	_printf
	jmp	L2
L7:
	addl	$29, 28(%esp)		x = x + 10*2 + 9
	jmp	L2
L3:
	movl	$LC2, (%esp)
	call	_puts				printf被优化成了puts 
	addl	$5, 28(%esp)		x = x + 5
	jmp	L2
L9:
	movl	$0, 28(%esp)		x=0
	nop
L2:
	movl	$0, %eax
	leave
	ret
	.ident	"GCC: (tdm64-1) 4.9.2"
	.def	_printf;	.scl	2;	.type	32;	.endef
	.def	_puts;	.scl	2;	.type	32;	.endef

可以看出来编译器自动帮我们把case排了个序还把中间缺的补上了。而跳转计算的部分还是一模一样的代码

  • 13
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值