Switch与if…else
有以下两个函数,y值根据x值进行加法运算,功能相同,SumSwitch()是switch版本,SumIfElse()是if else版本:
public int SumSwitch(int x,int y){
switch(x){
case 1:
y+=1;
break;
case 2:
y+=2;
break;
case 3:
y+=3;
break;
case 4:
y+=4;
break;
case 5:
y+=5;
break;
case 6:
y+=6;
break;
case 7:
y+=7;
break;
case 8:
y+=8;
break;
case 9:
y+=9;
break;
default:
y=0;
break;
}
return y;
}
public int SumIfElse(int x,int y){
if(x==1){
y+=3;
}
else if(x==2){
y+=4;
}
else if(x==3){
y+=5;
}
else if(x==4){
y+=6;
}
else if(x==5){
y+=7;
}
else if(x==6){
y+=8;
}
else if(x==7){
y+=9;
}
else if(x==8){
y+=10;
}
else if(x==9){
y+=11;
}
else{
y=0;
}
return y;
}
}
用Monodevelop的AOT编译器编译后的汇编代码如下:
SumSwitch:
_AlgorithmTest_SumSwitch_int_int:
00000b40 pushl %ebp
00000b41 movl %esp, %ebp
00000b43 pushl %ebx
00000b44 pushl %edi
00000b45 pushl %esi
00000b46 subl $0xc, %esp
00000b49 movl 0xc(%ebp), %esi
00000b4c movl 0x10(%ebp), %edi
00000b4f calll 0xb54
00000b54 popl %ebx
00000b55 addl $0x4ac, %ebx ## imm = 0x4AC
00000b5b decl %esi
00000b5c cmpl $0x9, %esi //比较x与9,如果x大于等于9(x在上一行有减1)
00000b5f jae 0xb9d //则跳转到b9d行,既是源码的default处。
00000b61 movl %esi, %ecx
00000b63 shll $0x2, %ecx //计算case的偏移量
00000b66 movl 0x14(%ebx), %eax //跳转表取址
00000b6c addl %ecx, %eax //通过case的偏移量访存取接下来代码的绝对地址
00000b6e movl (%eax), %eax //跳转表取址
00000b70 jmpl *%eax //跳转表跳转
00000b72 incl %edi //case1:+1
00000b73 jmp 0xb9f //跳到return处
00000b75 addl $0x2, %edi //case2:+2
00000b78 jmp 0xb9f //跳到return处
00000b7a addl $0x3, %edi //下同
00000b7d jmp 0xb9f
00000b7f addl $0x4, %edi
00000b82 jmp 0xb9f
00000b84 addl $0x5, %edi
00000b87 jmp 0xb9f
00000b89 addl $0x6, %edi
00000b8c jmp 0xb9f
00000b8e addl $0x7, %edi
00000b91 jmp 0xb9f
00000b93 addl $0x8, %edi
00000b96 jmp 0xb9f
00000b98 addl $0x9, %edi
00000b9b jmp 0xb9f
00000b9d xorl %edi, %edi
00000b9f movl %edi, %eax //跳转表出口
00000ba1 leal -0xc(%ebp), %esp
00000ba4 leal -0xc(%ebp), %esp
00000ba7 popl %esi
00000ba8 popl %edi
00000ba9 popl %ebx
00000baa leave
00000bab retl
00000bac nopl (%eax)
SumIfElse:
_AlgorithmTest_SumIfElse_int_int:
00000bb0 pushl %ebp
00000bb1 movl %esp, %ebp
00000bb3 pushl %ebx
00000bb4 pushl %edi
00000bb5 pushl %esi
00000bb6 subl $0xc, %esp
00000bb9 movl 0xc(%ebp), %esi
00000bbc movl 0x10(%ebp), %edi
00000bbf calll 0xbc4
00000bc4 popl %ebx
00000bc5 addl $0x43c, %ebx ## imm = 0x43C
00000bcb cmpl $0x1, %esi //比较x与1
00000bce jne 0xbd8 //如果不等于1,跳转到行bd8
00000bd0 addl $0x3, %edi //如果等于1,则加3
00000bd3 jmp 0xc2f //赋值后跳转到函数c2f行,跳出if else区域。
00000bd8 cmpl $0x2, %esi //比较x与2
00000bdb jne 0xbe5 //以下同上
00000bdd addl $0x4, %edi
00000be0 jmp 0xc2f
00000be5 cmpl $0x3, %esi
00000be8 jne 0xbf2
00000bea addl $0x5, %edi
00000bed jmp 0xc2f
00000bf2 cmpl $0x4, %esi
00000bf5 jne 0xbfc
00000bf7 addl $0x6, %edi
00000bfa jmp 0xc2f
00000bfc cmpl $0x5, %esi
00000bff jne 0xc06
00000c01 addl $0x7, %edi
00000c04 jmp 0xc2f
00000c06 cmpl $0x6, %esi
00000c09 jne 0xc10
00000c0b addl $0x8, %edi
00000c0e jmp 0xc2f
00000c10 cmpl $0x7, %esi
00000c13 jne 0xc1a
00000c15 addl $0x9, %edi
00000c18 jmp 0xc2f
00000c1a cmpl $0x8, %esi
00000c1d jne 0xc24
00000c1f addl $0xa, %edi
00000c22 jmp 0xc2f
00000c24 xorl %eax, %eax
00000c26 addl $0xb, %edi
00000c29 cmpl $0x9, %esi //x与9比较,
00000c2c cmovnel %eax, %edi //用条件传送决定加11或赋值0
00000c2f movl %edi, %eax
00000c31 leal -0xc(%ebp), %esp
00000c34 leal -0xc(%ebp), %esp
00000c37 popl %esi
00000c38 popl %edi
00000c39 popl %ebx
00000c3a leave
00000c3b retl
00000c3c nopl (%eax)
SumIfElse的汇编代码与源码基本相同,既是将x依次与1-9比较,直到找出相等的值(见注释)。SumSwitch与SumIfElse的版本相比主要是优化在两个地方。
第一是汇编代码会先判断x是否属于default(既不在1-9的范围呢):
00000b5c cmpl $0x9, %esi //比较x与9,如果x大于等于9(x在上一行有减1)
00000b5f jae 0xb9d //则跳转到b9d行,既是源码的default处。
而SumIfElse要将x与1-9全比对完毕才会决定是否要选择else的case。由于参数x的可能范围很大,SumSwitch先判定可能性大的case是合理的。
第二是跳转表,根据x的值直接跳转到对应的case:
00000b70 jmpl *%eax
在这行代码之前编译器通过计算将跳转表的基址与x*2的值相加并存到 %eax中,然后直接跳转到此地址。
乘除与移位
以下是一个将全局变量i乘以2并返回的函数,分别用乘法与左移实现:
public int Sum1(){
int i2=i*2;
return i2;
}
public int Sum2(){
int i22=i<<1;
return i22;
}
_AlgorithmTest_Sum1:
00000a80 pushl %ebp
00000a81 movl %esp, %ebp
00000a83 pushl %ebx
00000a84 subl $0x4, %esp
00000a87 calll 0xa8c
00000a8c popl %ebx
00000a8d addl $0x574, %ebx ## imm = 0x574
00000a93 movl 0x8(%ebp), %eax
00000a96 movl 0x8(%eax), %eax
00000a99 shll %eax //编译器将乘法换成了计算左移
00000a9b leal -0x4(%ebp), %esp
00000a9e leal -0x4(%ebp), %esp
00000aa1 popl %ebx
00000aa2 leave
00000aa3 retl
00000aa4 nopw %cs:(%eax,%eax)
_AlgorithmTest_Sum2:
00000ab0 pushl %ebp
00000ab1 movl %esp, %ebp
00000ab3 pushl %ebx
00000ab4 subl $0x4, %esp
00000ab7 calll 0xabc
00000abc popl %ebx
00000abd addl $0x544, %ebx ## imm = 0x544
00000ac3 movl 0x8(%ebp), %eax
00000ac6 movl 0x8(%eax), %eax
00000ac9 shll %eax //计算左移
00000acb leal -0x4(%ebp), %esp
00000ace leal -0x4(%ebp), %esp
00000ad1 popl %ebx
00000ad2 leave
00000ad3 retl
00000ad4 nopw %cs:(%eax,%eax)
两段汇编代码的内容完全一样,目前的编译器基本上默认会将简单的乘法转为左移(注释处)。
以下是一个将全局变量i除以2并返回的函数,分别用除法与右移实现:
public int Sum3(){
int i3=i/2;
return i3;
}
public int Sum4(){
int i4=i>>1;
return i4;
}
_AlgorithmTest_Sum3:
00000ae0 pushl %ebp
00000ae1 movl %esp, %ebp
00000ae3 pushl %ebx
00000ae4 subl $0x4, %esp
00000ae7 calll 0xaec
00000aec popl %ebx
00000aed addl $0x514, %ebx ## imm = 0x514
00000af3 movl 0x8(%ebp), %eax
00000af6 movl 0x8(%eax), %ecx
00000af9 movl %ecx, %eax
00000afb shrl $0x1f, %eax //逻辑右移31位,%eax变为全0或全1
00000afe addl %ecx, %eax //相当于将%ecx的值移到%eax中
00000b00 sarl %eax //将除法变为计算右移
00000b02 leal -0x4(%ebp), %esp
00000b05 leal -0x4(%ebp), %esp
00000b08 popl %ebx
00000b09 leave
00000b0a retl
00000b0b nopl (%eax,%eax)
_AlgorithmTest_Sum4:
00000b10 pushl %ebp
00000b11 movl %esp, %ebp
00000b13 pushl %ebx
00000b14 subl $0x4, %esp
00000b17 calll 0xb1c
00000b1c popl %ebx
00000b1d addl $0x4e4, %ebx ## imm = 0x4E4
00000b23 movl 0x8(%ebp), %eax
00000b26 movl 0x8(%eax), %eax
00000b29 sarl %eax //计算右移
00000b2b leal -0x4(%ebp), %esp
00000b2e leal -0x4(%ebp), %esp
00000b31 popl %ebx
00000b32 leave
00000b33 retl
00000b34 nopw %cs:(%eax,%eax)
两个版本的汇编代码都使用了算术右移。
结论:在case很多的情况下,switch会先判断default,然后只用一次跳转即可完成整个判断过程,相对于if…else按固定顺序进行依次判断可省去很多指令。另外,编译器会将乘除自动优化为移位运算,所以在源码中选择乘除或位移的写法就没有区别了。
维护日志:
2020-2-3:review