如果我们现在要写一个函数,参数为两个整数,要求返回这两个整数差的绝对值,我们会有如下的写法。
int diff(int a,int b)
{
if(a<b)
return b-a;
else
return a-b;
}
现在给出另一种写法
int diff(int a,int b)
{
int x1=a-b;
int x2=b-a;
if(a<b)
return x2;
else
return x1;
}
第一种叫做条件控制,第二种叫做条件传送,结合代码很容易理解为什么这样称呼他们。而且在保证随机输入的情况下,第二种的运行速度是比第一种要快的。而且从事实角度出发,如果你写出了第一种代码,编译器也会自动帮你优化成第二种。来看下面的代码。
#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
int ka[100000],kb[100000];
void init()
{
for(int i=0;i<100000;i++)
{
ka[i]=rand()%10;
kb[i]=rand()%10;
}
}
int diff(int a,int b)
{
if(a<b)
return b-a;
else
return a-b;
}
int main ()
{
struct timeval start;
struct timeval end;
init();
gettimeofday(&start,NULL);
for(int i=0;i<100000;i++)
{
diff(ka[i],kb[i]);
//diff(i,i+1);
}
gettimeofday(&end,NULL);
unsigned long d;
d=1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
printf("the difference %ld\n",d);
}
我将用上边的代码来测试两种写法的性能。
首先对上边的代码使用GCC进行不优化的编译。指令为 gcc -Og -S a.c
编译器忠实的翻译了我们的代码,比较两个参数,通过ZF决定是否跳转,在两个分支里分别计算所需要的结果。
使用O1级别的优化。指令为 gcc -O1 -S a.c
在优化过后,正如上边第二种写法的思路,先计算两种可能的值,然后再根据两个参数之间的大小关系,决定把哪一个值作为返回值。
存在这种优化的原因是处理器是通过使用流水线来获得高性能,在流水线中,一条质量的处理要经过一个系列的阶段,每个阶段执行所需操作的一小部分(例如,从内存中取指令,确定指令类型,从内存中读数据,执行算术运算,向内存中写数据,以及更新程序计数器)。这种方法通过重叠连续指令的步骤来获得高性能,例如在取一条指令的同事,执行它前面一条指令的算术运算。要做到这一点,要求能够事先确定要执行的指令序列,这样才能保持流水线中充满了待执行的指令。当机器遇到条件跳转的时候,如果跳转成功,那么意味着计算机之前预先装入流水线的指令都错了(计算机总是按顺序预装指令),所有的指令都要被重新装入(跳转后的指令才是正确的),所以跳转意味着程序性能的严重下降。
条件传送相当于把原本可能浪费在跳转的时间用在了计算另外一条分支上,所获得的性能提升取决于跳转所浪费的时间和计算另外一条分支的时间对比。不过从另一点来看,由于只有最后返回之前才进行条件的判断,条件传送更有利于流水线一直处于满的状态,运行时间更加稳定。
道理虽然是这样的,但是在我本机的实际测试中,并没有明显的性能差异,原因我推测可能是现代处理器应对指令的重装已经不再是十分耗时的过程了。
再来看__bulitin_expect,既然我们知道跳转会造成严重的效率损失,那么程序的编写者知道自己写的某一个if语句在大多数情况下会走向哪一个分支时,可以选择使用__bulitin_expect这个宏来告诉编译器这个if更有可能会选择哪一个分支,从而让编译器生成出跳转可能比较小的汇编代码。