- 注册时间
- 2007-12-27
- 最后登录
- 1970-1-1
- 威望
- 星
- 金币
- 枚
- 贡献
- 分
- 经验
- 点
- 鲜花
- 朵
- 魅力
- 点
- 上传
- 次
- 下载
- 次
- 积分
- 40142
- 在线时间
- 小时
|
楼主 |
发表于 2008-2-26 14:50:29
|
显示全部楼层
循环变量的优化
现在来介绍编译器如何识别循环变量的问题。
比如前面各种关于循环的优化中,我们如果要计算依赖向量,起码编译器要能够识别循环变量,
而识别循环变量也是编译器的一个优化过程。
而通常编译器所认为的循环变量可能同我们所看到的有所不同。
通常的循环变量是狭义的,也就是说,如果这个变量在一个循环的任意两次相邻的迭代中变换值是常数,
那么这个变量就是循环变量。最常见的循环变量是如下代码中:如果循环体中没有对变量i的修改,那么i就必然是一个循环变量。
但是如果循环体中对i做了修改,那么虽然看上去i还像是一个循环变量,但是对于编译器来说,i已经不是循环变量了,如:- for(i=0;i<n;i++){
- if(..){
- i++;
- }
- ...
- }
复制代码 像上面的循环体,有时候i增加1,有时候i增加2,那么i就不是循环变量了。
而还有一些情况,循环变量人工不容易看出来,但是编译器确可以判断出来,如:- i=0;
- while(...){
- j=i+1;
- ...
- k=j+2;
- ...
- i=k;
- ...
- }
复制代码 像上面的代码,如果没有其他对j,k,i的修改,那么这里i,j,k实际上都是循环变量,
其中每次迭代这三个变量都增加了3.
而对于编译器来说,通常还可以识别一些更加复杂的循环变量,如:- i=0;
- while(...){
- j=i+1;
- ...
- k=j+2;
- h=a*k+j;
- ...
- i=k;
- u=h-i+b;
- ...
- }
复制代码 像上面代码中,编译器首先可以识别出i,j,k是循环变量,然后编译器发现h,u是循环变量的线性组合,
所以编译器可以识别出它们也是循环变量。(其中a,b可以是变量,只要在循环体内部没有被修改)
比如h每个循环改变的量为3*(a+1),u每个循环改变的量为3*a,编译器可以通过将上面代码改变为:- i=0;
- h=3*a+1;
- u=3*a+1+b;
- step1=3*(a+1);
- step2=3*a;
- while(...){
- j=i+1;
- ...
- k=j+2;
- ///h=a*k+j;///删除原先这里对h的定义
- ...
- i=k;
- ///u=h-i+b;///删除这里对u的定义
- ...
- h+=step1;
- u+=step2;
- }
复制代码 在经过这个变换以后,在循环体内部, 所有关于循环变量的计算均可以不包含乘法运算,从而比原先代码应该可以快一些。
同样,如果在编译器优化比较后面的部分,通常,对于数组的访问都已经被展开,
如代码- for(i=0;i<n;i++){
- a[i] =....;
- }
复制代码 可能被展开成:- for(i=0;i<n;i++){
- p=a+i*S; ///这里S是常数,代表数组a中每个元素在内存中占用的空间大小
- *p=...;
- }
复制代码 那么对于编译器来说,指针p也是一个循环变量,所以代码可以被转化为- p=a;
- for(i=0;i<n;i++){
- *p=...;
- p=p+S;
- }
复制代码 变化以后同样计算地址中的乘法运算被消除了。
我看到郭给出链接中一篇英文文章中介绍到对于数组,最好让每个元素数据的大小是2的幂,这样,计算每个元素的地址时候,
乘法就可以被移位替换掉,从而提高了速度。但是,如果那样的数组通常都是被通过循环变量访问的,我们可以看出来,完全没有
必要做那样的优化(实际上那样可能会消耗更多的内存空间,从而性能更加差).
此外,有一些比较优秀的程序员,他们知道计算机计算移位比乘法运算快,所以对于下面的代码- for(i=0;i<n;i++){
- a[2*i]=...;
- ...
- }
复制代码 他们可能写成了- for(i=0;i<n;i++){
- a[i<<1]=...;
- ...
- }
复制代码 其实,对于编译器来说,反而前面的代码更加容易优化,因为编译器可以非常容易识别出2*i是一个循环变量,从而我们可以计算依赖向量,
做一些前面提到过的如么模变换,仿射变换之类的优化。反而对于后面的代码,由于通常编译器是不会将移位运算转化为乘法运算的,所以
通常的编译器反而无法知道后面的i<<1也是一个循环变量,从而阻止了进一步优化的可能。
此外,部分编译器还会对一些循环变量之间的相乘做优化(比如Open64),比如代码:- i=0;
- while(...){
- j=i+1;
- ...
- k=j+2;
- h=a*k+j;
- ...
- i=k;
- u=h-i+b;
- ...
- sum+=h*u;
- }
复制代码 在编译器分析出h和u都是循环变量以后,编译器就可以对h*u做进一步优化
我们知道 h=h0+i*step1,u=u0+i*step2;
所以h*u=h0*u0+(h0*step2+u0*step1)*i+i*i*step1*step2
分别对于i和i+1计算上面的表达式并相减,我们可以得到对于第i次迭代,h*u的变换值是
h0*step2+u0*step1+step1*step2+i*2*step1*step2;
所以我们知道,上面代码于是可以优化成:- i=0;
- h=3*a+1;
- u=3*a+1+b;
- step1=3*(a+1);
- step2=3*a;
- hu=h*u;
- ddhu=2*step1*step2;
- dhu=h0*step2+u0*step1+step1*step2+ddhu*i;
- while(...){
- j=i+1;
- ...
- k=j+2;
- ///h=a*k+j;///删除原先这里对h的定义
- ...
- i=k;
- ///u=h-i+b;///删除这里对u的定义
- ...
- h+=step1;
- u+=step2;
- sum+=hu;
- hu+=dhu;
- dhu+=ddhu;
- }
复制代码 从而计算循环体内计算h*u的乘法运算由两次加法运算代替,从而提高了速度。
同样道理,对于三个循环变量的相乘,从理论上,我们同样可以转化为若干次加法运算。
不过据我所知,并没有编译器真正这样去做,毕竟实际中,这样代码的例子会非常少见。
当然,如果换成用郭的HugeCalc代码中大整数做循环变量的代码,那么遇到上面的代码编译器
的优化同样无能为力了,那么就需要手工做类似的优化了。 |
|