3.6.5 使用条件传送实现条件分支
编译器将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
假设有如下的C语言代码块:
long lt_cnt = 0;
long ge_cnt = 0;
long absdiff_se(long x, long y)
{
long result;
if(x < y){
lt_cnt++;
result = y - x;
}
else{
ge_cnt++;
result = x - y;
}
return result;
}
与其对应的汇编代码的控制流代码可以通过goto语句重新写成:
(注意:使用goto语句通常被认为是不好的编程风格,因为这样会使得代码非常难以阅读和调试,这里使用goto语句仅仅是为了构造描述汇编代码程序控制流的C程序)
long gotodiff_se(long x, long y)
{
long result;
if(x < y)
goto x_ge_y;
lt_cnt++;
result = y - x;
return result;
x_gee_y:
ge_cnt++;
result = x - y;
return result;
}
通过编译器将其转换成汇编代码如下所示:
//long absdiff_se(long x, long y)
/* x in %rdi, y in %rsi */
adsdiff_se:
cmpq %rsi, %rdi // compare x:y
jge .L2 // if >= goto x_ge_y
addq $1, lt_cnt(%rip) // it_cnt++
movq %rsi, %rax
subq %rdi, %rax // result = y - x
ret // return
.L2 //x_ge_y:
addq $1, ge_cnt(%rip) // ge_cnt++
movq %rdi, %rax
subq %rsi, %rax // result = x - y
ret // return
实现条件操作的传统方式是通过使用控制的条件转移。当条件满足时,程序沿着一条执行路径执行,而当条件不满足时,就走另一条路径。这种机制简单而通用,但是在现在处理器上,可能非常低效。其实我们还可以通过条件赋值的形式来重新编译实现这段C语言代码:
long cmovdiff(long x, long y)
{
long rval = y-x;
long eval = x-y;
long ntest = x >= y;
/* Line below requires single instruction */
if(ntest) rval = eval;
return rval;
}
将其转换成汇编代码如下所示:
//long absdiff(long x, long y)
//x in %rdi, y in %rsi
absdiff:
movq %rsi, %rax
subq %rdi, %rax //rtval = y - x
movq %rdi, %rdx
subq %rsi, %rdx //eval = x -y
cmpq %rsi, %rdi //compare x:y
cmovge %rdx, %rax //if >= , rval = eval
ret //return tval
为什么基于条件数据传送的代码会比基于条件控制转移的代码性能要好?
这涉及到关于处理器如何运行的知识。现代的处理器通过 流水线(pipeline) 来获得高性能,在流水线中需要经过一系列的阶段,每个阶段执行所需操作的一小部分(例如:从内存取指令、确定指令类型、从内存读数据、执行算数运算、向内存写数据、更新程序计数器)。这种方法通过重叠连续指令的方法来获得高性能,例如在取一条指令的同时,执行它前面一条指令的算术运算。要做到这一点要求能够确定要执行的 指令序列 这样才能保持流水线中充满了待执行的指令。
而当及其遇到条件跳转(也称为“分支”)时,只有当条件分支执行完了之后 ,才能决定分支往哪里走。处理器采用非常精密的 分支预测逻辑 来猜测每条跳转指令是否会执行。只要猜测比较可靠(现代微处理器设计试图达到90%以上的成功率),指令流水中就会充满着指令。另一方面,错误预测一个跳转,要求处理器丢掉它为该跳转指令后所有指令已做的工作,然后再开始用从正确位置处其实大约15~30个时钟周期,导致程序性能的下降。
但是使用条件传送也不总是会提高代码的效率。例如,如果 then-expr或者 else-expr的求值需要大量的计算,那么当相对应的条件不满足的时候,这些工作就白白浪费了。编译器必须考虑浪费的计算和由于分支预测错误所造成的性能处罚之间的相对性能。说实话,编译器并不具有足够的信息来做出可靠的决定;例如他们不知道分支会多好的遵行可预测的模式。实验表明,只有当两个表达式都很容易计算时,例如表达式分别都只是一条加法指令,他才会使用条件传送。根据经验,即使许多分支预测错误的开销会超过更复杂的计算,GCC还是会使用条件控制转移。