经常听说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排了个序还把中间缺的补上了。而跳转计算的部分还是一模一样的代码