在编译器眼里,一切简单的东西都复杂了,一切为了CPU服务。我们来看编译器是怎么处理关系及逻辑运算的,又是怎么对其进行优化的。gogogo!
计算机是如何判断"==","!=",">=","<=",">","<"的?
在VC6.0中,是利用各种类型的跳转来实现两者间的关系比较的,根据比较结果所影响的标记位来选择对应的条件跳转指令。如何选择条件跳转指令,需要根据两个进行比较的数值所知用到的关系运算,不同的关系运算对应的跳转指令也不相同。各种关系对应的条件跳转指令如下:指令助记符 检查标记位 说明
JZ ZF==1 等于0则跳转
JE ZF==1 相等则跳转
JNZ ZF==0 不等于0则跳转
JNE ZF==0 不相等则跳转
JS SF==1 符号为负则跳转
JNS SF==0 符号为正则跳转
JP/JPE PF==1 "1"的个数为偶数则跳转
JNP/JPO PF==0 "1"的个数为奇数则跳转
JO OF==1 溢出则跳转
JNO OF==0 无溢出则跳转
JC CF==1 进位则跳转
JB CF==1 小于则跳转
JNAE CF==1 不大于等于则跳转
JNC CF==0 无进位则跳转
JNB CF==0 不小于则跳转
JAE CF==0 大于则跳转
JBE CF==1或ZF==1 小于等于则跳转
JNA CF==1或ZF==1 不大于则跳转
JNBE CF==0或ZF==0 不小于等于则跳转
JA CF==0或ZF==0 大于则跳转
JL SF!=OF 小于则跳转
JNGE SF!=OF 不大于等于则跳转
JNL SF==0F 不小于则跳转
JGE SF==OF 不大于等于则跳转
JLE ZF!=OF或ZF==1 小于等于则跳转
JNG ZF!=OF或ZF==1 不大于则跳转
JNLE SF==OF且ZF==0 不小于等于则跳转
JG SF==OF且ZF==0 大于则跳转
通常情况下,这些跳转指令都与CMP和TEST匹配出现,但条件跳转指令检查的是标记位。因此,在有修改标记位的代码处,也可以根据需要使用条件跳转指令来修改程序流程。
表达式短路是什么?
表达式短路通过逻辑与运算和逻辑或运算使语句根据条件在执行时发生中断,从而不执行后面的语句。如何利用表达式短路来实现语句中断呢?根据逻辑与和逻辑或运算的特性,如果是与运算,当运算符左边的语句块为假时,则直接返回假值,不执行右边的语句;如果是或运算,当运算符左边的语句块为真实,直接返回真值,不执行右边的语句块。
我们来看个例子:(这个函数的功能是递归方式求累加和)
int Accumulation(int number){
//当number等于0时,逻辑与运算符左边的值为假,将不会执行右边的语句
//形成表达式短路,从而找到递归出口
number && (number += Accumulation(number - 1));
return number;
}
//C++与反汇编代码对比说明
int Accumulation(int number){
//C++
number && (number += Accumulation(number - 1));
//反汇编
cmp dword ptr [ebp+8],0 //比较变量number是否等于0
je Accumulation+35h //通过JE跳转,检查ZF标记位等于1跳转(跳到return number;)
mov eax,dword ptr [ebp+8]//跳转失败,进入递归调用
sub eax,1 //对变量number减1
push eax //参数入栈
call @ILT+30(Accumulation)//继续调用Accumulation函数
add esp,4 //调用完成后释放栈空间(只有参数入栈了,没有局部变量)
mov ecx,dword ptr [ebp+8] //把变量number放入ecx
add ecx,eax //ecx加上函数的返回值(这个函数使用eax传递返回值)
mov dword ptr [ebp+8],ecx //把ecx中的值赋给变量number
//C++
return number;
//反汇编
mov eax,dword ptr [ebp+8] //使用eax传递返回值
}
条件表达式怎么转换?
条件表达式是三目运算,根据表达式1得到的结果进行选择。如果是真值,选择执行表达式2;如果是假值,选择表达式3执行。语句构成如下:
表达式1?表达式2:表达式3
条件表达式也属于表达式,所以条件表达式可以嵌套。嵌套后,其执行顺序依然是由左向右,由内向外。
条件表达式的构成因该是先判断再选择。但是,编译器并不一定会按这种方式进行编译,当表达式2与表达式3都是常量时,条件表达式可以被优化;当表达式2或表达式3中的一个为变量时,条件表达式不可以被优化,会被转换程分支结构。当表达式1位一个常量时,编译器在编译期间得到答案,不会有条件表达式存在。我们来看一下编译器是怎么优化,如何避免使用分支结构的。
条件表达式有如下4中转换方案:
方案1>表达式1为简单比较,而表达式2和表达式3两者的差值等于1;
方案2>表达式1为简单比较,而表达式2和表达式3两者的差值大于1;
方案3>表达式1为复杂比较,而表达式2和表达式3两者的差值大于1;
方案4>表达式2和表达式3有一个为变量,于是无法优化。
方案1:
//C++及反汇编代码讲解
//C++源码
int Condition(int argc,int n){
//比较参数argc是否等于5,真值返回5,假值返回6
return argc == 5 ? 5: 6;
}
//C++
return argc == 5 ? 5 : 6;
//反汇编
xor eax,eax //清空eax
cmp dword ptr [ebp+8],5 //比较argc和5
setne al //setne检查ZF标记位,当ZF==1,则赋值al等与0,反之则赋值al为1
add eax,5 //若argc==5,则al==0,反之al==1,执行这句后,eax正好为5或6
方案2:
//C++及反汇编代码讲解
//C++源码
int Condition(int argc,int n){
return argc == 5 ? 4 : 10;
}
//C++
return argc == 5 ? 4 : 10;
//反汇编
mov eax,dword ptr [ebp+8] //把argc放入eax
sub eax,5 //argc-5
neg eax //求补运算,eax符号位发生变化,CF置1
sbb eax,eax //执行借位减法指令,这句相当与eax = eax - eax - CF
and eax,6 //当CF为1时,eax为0xFFFFFFFF,否则为0.如果eax==-1,执行这句后,
//eax为6,如果eax==0,eax为0
add eax,4 //加4后正好为4或10
方案3:
//C++及反汇编代码讲解
//C++源码
int Condition(int argc,int n){
return argc <= 8 ? 4:10;
}
//C++
return argc <= 8 ? 4 : 10;
//反汇编
xor eax,eax //清空eax
cmp dword ptr [ebp+8],8 //比较argc和8
setg al //根据比较结果,使用setg指令,当标记位SF=OF且ZF=0,赋值al为1
//即,argc>8,如果大于赋值为1
dec eax //执行自减操作,eax为0xFFFFFFFF或0
and al,OFAh //al与0xFA位与,eax为0xFFFFFFFF或0,(0FAh为表达式2减表达式3的数值)
add eax,0Ah //加上0x0A后结果必然为4或10
通过分析我们可以看出,编译器会尽可能避免分支语句。这是因为处理器会预读指令,顺序结构可以提高命中率,提高运行效率。