循环不变量的优化
转自:http://blog.csdn.net/mathe/article/details/1175620
在九十年代末时,我一个同学在写一个处理医学图像的程序,里面用了不少三角函数 ,所以程序运行很慢(那时机器也慢,可能主频都在100M左右吧),处理一个图片都要20多秒。然后他向我询问,有没有什么办法可以提高运行速度。
我看了一下他的代码,做了下简单的修改,速度一下子就提高了3倍多。原因在于,他的代码里面有一些循环不变量,可以做很简单的优化,比如他的代码如下:
for(x=0;x<S_x;x++){
for(y=0;y<S_y; y++){
for(z=0;z<S_z;z++){
sum+= sin(x)*A[x][y][z]+sin(y)*B[x][y][z]+sin(z)*C[x][y][z];
}
}
}
象这样的代码,每次循环z进来以后,其实x,y都不会发生变化,所以没有必要重新计算sin(x),sin(y).可以由于sin(x),sin(y)是函数调用,编译器不一定能够知道这里函数调用不需要重复产生。所以如果完全让编译器去做,它就不一定能够消除这些重复的函数调用,那么运行速度自然就会比较慢,而如果我们把上面代码改成:
for(x=0;x<S_x;x++){
double sinx=sin(x);
for(y=0;y<S_y; y++){
double siny=sin(y);
for(z=0;z<S_z;z++){
sum+= sinx*A[x][y][z]+siny*B[x][y][z]+sin(z)*C[x][y][z];
}
}
}
那么我们就可以通过手工的方法,将循环不变量(关于循环z)提升到循环z的外面,从而减少了对这种函数调用的访问,从而提高了速度。
当然这种优化,现在有些编译器已经能够对部分表达式做到,比如象上面的sin(.)函数,编译器可以事先识别一些常数的库函数,比如三角函数等,它知道这些函数不会有副作用,所以对这些函数,使用相同参数的重复调用就可以消除了。但是对于更多的情况,编译器还是无法分析,这就需要我们在写程序时,多加注意,从而能够写出质量更高的代码。
比如对于下面的函数:
int sqr_sum(double *err, double a[], int size_a, double b[], int size_b){
int i;
if(err==NULL||size_a!=size_b)
return 0;
*err=0;
for(i=0;i<size;i++)
*err+=(a[i]-b[i])*(a[i]-b[i]);
return 1;
}
这是一个非常常见的代码,但是它的效率就不够高,最主要的原因是循环里面要反复访问内存*err.
这个循环内部的代码展开后实际类似:
load *err;
load a[i];
load b[i];
计算 ...
store *err;
由于err是个指向double类型的指针,编译器无法判断err是否会指向数组a[.],b[.],所以上面的四个内存访问都有可能访问到同一个内存地址,这这种情况下,编译器就无法交换它们读写内存的顺序,从而,无法做进一步的优化。
但是如果我们将代码改写为:
int sqr_sum(double *err, double a[], int size_a, double b[], int size_b){
int i;
double local_err;
if(err==NULL||size_a!=size_b)
return 0;
local_err=0;
for(i=0;i<size;i++)
local_err+=(a[i]-b[i])*(a[i]-b[i]);
*err = local_err;
return 1;
}
那么,这个代码的性能将会高很多。首先,编译器可以将局部变量local_err放在寄存器中,从而所有对local_err的访问都不需要经过内存,从而减少了内存访问的次数,这提高了访问速度,而且减少了指令数目。
其次,由于编译器知道local_err同数组a[],b[]等的内存都不重叠,从而这个循环的每两次执行的语句访问的内存空间必然完全不同,我们完全可以让这些不同语句并行执行。那么在支持SSE的机器上,我们就可以让多条语句由一条SSE语句来并行执行。同时,对于多CPU的机器,我们可以让多个CPU来并行执行,比如第一个CPU累加前面的部分,第二个CPU累加后面部分,完成以后,在统一累加一次就可以了。
更多关于编译器优化的介绍请看:
http://bbs.emath.ac.cn/thread-173-1-1.html