一个好的编译器一定是一群顶尖软件高手们集体长时间创作的作品了,所以研究研究编译器的编译过程就是在向这些高手们学习。说到底,编译器也是一个很好的老师了,它可以为我们揭开高级语言实现的奥秘,为那些永不满足于表面现象的程序员深入掌握一门语言提供一个很好的途径。 来看看微软的cl 8.0编译器是怎样来编译C语言的switch语句的,照例写一个测试的例子:
int test()
{
inti,j;
i=j=0;
switch(i)
{
case1:j+=1;break;
case2:j+=2;break;
default:j+=5;
}
return0;
}
看看生成的汇编代码:
; 5 :
; 6 : switch(i)
movecx, DWORD PTR _i$[ebp]
movDWORDPTR tv64[ebp], ecx
;case 1:
cmp DWORD PTR tv64[ebp], 1
je SHORT$LN3@test
;case 2:
cmp DWORD PTR tv64[ebp], 2
je SHORT$LN2@test
;default:
jmp SHORT $LN1@test
$LN3@test:
; 7 : {
; 8 : case1:j+=1;break;
movedx, DWORD PTR _j$[ebp]
addedx,1
movDWORDPTR _j$[ebp], edx
jmp SHORT $LN4@test
$LN2@test:
; 9 : case2:j+=2;break;
moveax, DWORD PTR _j$[ebp]
addeax,2
movDWORDPTR _j$[ebp], eax
jmp SHORT $LN4@test
$LN1@test:
; 10 : default:j+=5;
movecx, DWORD PTR _j$[ebp]
addecx,5
movDWORDPTR _j$[ebp], ecx
$LN4@test:
; 11 : }
; 12 :
; 13 : return0;
xoreax, eax
; 14 : }
这段汇编代码是很好理解的,就是比较并跳转的过程。
下面看看一个比较复杂的switch
int test()
{
inti,j;
i=j=0;
switch(i)
{
case1:j+=1;break;
case2:j+=2;break;
case3:j+=3;break;
case4:j+=4;break;
default:j+=10;
}
return0;
}
再来看看编译器生成的汇编代码:
; 5 :
; 6 : switch(i)
movecx, DWORD PTR _i$[ebp]
movDWORDPTR tv64[ebp], ecx
mov edx, DWORD PTR tv64[ebp]
subedx, 1
movDWORDPTR tv64[ebp], edx
cmp DWORD PTR tv64[ebp], 3
ja SHORT$LN1@test
mov eax, DWORD PTR tv64[ebp]
jmpDWORDPTR $LN10@test[eax*4]
;以上的汇编代码相当于以下的伪指令:
if(i-1 > 3)
{
goto $LN1@test;//跳转到default
}
else
{
goto $LN10@test[i-1];//case 1,case 2,case 3,case 4的情况
}
$LN10@test是一个跳转表,通过它就可以跳转到相应的处理代码中去,看来微软的编译器还是很聪明的,会在编译时对case的值作一些判断,这也就是所谓的优化吧。
$LN5@test:
; 7 : {
; 8 : case1:j+=1;break;
movecx, DWORD PTR _j$[ebp]
addecx,1
movDWORDPTR _j$[ebp], ecx
jmp SHORT $LN6@test
$LN4@test:
; 9 : case2:j+=2;break;
movedx, DWORD PTR _j$[ebp]
addedx,2
movDWORDPTR _j$[ebp], edx
jmp SHORT $LN6@test
$LN3@test:
; 10 : case3:j+=3;break;
moveax, DWORD PTR _j$[ebp]
addeax,3
movDWORDPTR _j$[ebp], eax
jmp SHORT $LN6@test
$LN2@test:
; 11 : case4:j+=4;break;
movecx, DWORD PTR _j$[ebp]
addecx,4
movDWORDPTR _j$[ebp], ecx
jmp SHORT $LN6@test
$LN1@test:
; 12 : default:j+=10;
movedx, DWORD PTR _j$[ebp]
addedx,10 ;0000000aH
mov DWORD PTR _j$[ebp], edx
$LN6@test:
; 13 : }
......
$LN10@test:
DD $LN5@test ;case 1
DD $LN4@test ;case 2
DD $LN3@test ;case 3
DD $LN2@test ;case 4
下面来看看加大各case的差别会带来怎样的差异:
int test()
{
inti,j;
i=j=0;
switch(i)
{
case1:j+=1;break;
case2:j+=2;break;
case9:j+=3;break;
case15:j+=4;break;
default:j+=10;
}
return0;
}
来看看编译器产生的汇编代码:
_test PROC
; 2 : {
.....
; 5 :
; 6 : switch(i)
movecx, DWORD PTR _i$[ebp]
movDWORDPTR tv64[ebp], ecx
mov edx, DWORD PTR tv64[ebp]
subedx, 1
movDWORDPTR tv64[ebp], edx
cmp DWORD PTR tv64[ebp], 14 ; 0000000eH
ja SHORT $LN1@test
mov eax, DWORD PTR tv64[ebp]
movzx ecx, BYTE PTR $LN10@test[eax]
jmpDWORDPTR $LN11@test[ecx*4]
以上的语句相当于下面的代码:
if(i-1>=14)
{
goto$LN1@test ;//跳转到switch语句的defaul分支
}
else
{
index= $LN10@test[i-1];//$LN10@test为跳转索引表
goto$LN11@test[index];//$LN11@teest为跳转表
}
可见,对于case的值有较大的差别的情况编译器还会新增一个跳转索引表,先通过case值找到对应的跳转表的索引,然后通过索引再找到跳转表中的跳转地址。
$LN5@test:
; 7 : {
; 8 : case1:j+=1;break;
movedx, DWORD PTR _j$[ebp]
addedx,1
movDWORDPTR _j$[ebp], edx
jmp SHORT $LN6@test
$LN4@test:
; 9 : case2:j+=2;break;
moveax, DWORD PTR _j$[ebp]
addeax,2
movDWORDPTR _j$[ebp], eax
jmp SHORT $LN6@test
$LN3@test:
; 10 : case9:j+=3;break;
movecx, DWORD PTR _j$[ebp]
addecx,3
movDWORDPTR _j$[ebp], ecx
jmp SHORT $LN6@test
$LN2@test:
; 11 : case15:j+=4;break;
movedx, DWORD PTR _j$[ebp]
addedx,4
movDWORDPTR _j$[ebp], edx
jmp SHORT $LN6@test
$LN1@test:
; 12 : default:j+=10;
moveax, DWORD PTR _j$[ebp]
addeax,10 ; 0000000aH
mov DWORD PTR _j$[ebp], eax
$LN6@test:
; 13 : }
; 14 :
; 15 : return0;
xoreax, eax
; 16 : }
movesp, ebp
pop ebp
ret 0
$LN11@test:
DD $LN5@test
DD $LN4@test
DD $LN3@test
DD $LN2@test
DD $LN1@test
$LN10@test:
DB 0
DB 1
DB 4
DB 4
DB 4
DB 4
DB 4
DB 4
DB 2
DB 4
DB 4
DB 4
DB 4
DB 4
DB 3
_test ENDP
我们再来进一步的加大case值的差别:
int test()
{
inti,j;
i=j=0;
switch(i)
{
case1:j+=1;break;
case100:j+=2;break;
case250:j+=3;break;
case550:j+=4;break;
default:j+=10;
}
return0;
}
来看看生成的汇编代码片断:
; 5 :
; 6 : switch(i)
movecx, DWORD PTR _i$[ebp]
movDWORDPTR tv64[ebp], ecx
;大于250则跳转到$LN10@test,即case 550 的情况
cmp DWORD PTR tv64[ebp], 250 ; 000000faH
jg SHORT $LN10@test
case 250
cmp DWORD PTR tv64[ebp], 250 ; 000000faH
je SHORT $LN3@test
;case 1
cmp DWORD PTR tv64[ebp], 1
je SHORT$LN5@test
;case 100
cmp DWORD PTR tv64[ebp], 100 ; 00000064H
je SHORT $LN4@test
;其它情况跳转到default处的代码
jmp SHORT $LN1@test
$LN10@test:
;case 550
cmp DWORD PTR tv64[ebp], 550 ; 00000226H
je SHORT $LN2@test
;其它情况跳转到default处的代码
jmp SHORT $LN1@test
$LN5@test:
哈哈,这与第一种情况很相似吧!好端端的代码被编译器处理得有点复杂,但是又滴水不漏。当然,编译器也是先判断了具体的值以后才对进行处理的。这时候,编译器一定在衡量了时间与空间的损失后作出这样的决定的,如果用第三种查表的方法继续编译空间损失会很大,需要一个很长的表格,在这种情况下,编译器选择了损失时间而节省空间。