循环变量的优化

现在来介绍编译器如何识别循环变量的问题。
比如前面各种关于循环的优化中,我们如果要计算依赖向量,起码编译器要能够识别循环变量,
而识别循环变量也是编译器的一个优化过程。
而通常编译器所认为的循环变量可能同我们所看到的有所不同。
通常的循环变量是狭义的,也就是说,如果这个变量在一个循环的任意两次相邻的迭代中变换值是常数,
那么这个变量就是循环变量。最常见的循环变量是如下代码中:
  1. for(i=0;i<n;i++){
  2.    ....
  3. }
复制代码
如果循环体中没有对变量i的修改,那么i就必然是一个循环变量。
但是如果循环体中对i做了修改,那么虽然看上去i还像是一个循环变量,但是对于编译器来说,i已经不是循环变量了,如:
  1. for(i=0;i<n;i++){
  2.   if(..){
  3.       i++;
  4.   }
  5.   ...
  6. }
复制代码
像上面的循环体,有时候i增加1,有时候i增加2,那么i就不是循环变量了。
而还有一些情况,循环变量人工不容易看出来,但是编译器确可以判断出来,如:
  1. i=0;
  2. while(...){
  3.    j=i+1;
  4.    ...
  5.    k=j+2;
  6.    ...
  7.    i=k;
  8.    ...
  9. }
复制代码
像上面的代码,如果没有其他对j,k,i的修改,那么这里i,j,k实际上都是循环变量,
其中每次迭代这三个变量都增加了3.
而对于编译器来说,通常还可以识别一些更加复杂的循环变量,如:
  1. i=0;
  2. while(...){
  3.    j=i+1;
  4.    ...
  5.    k=j+2;
  6.    h=a*k+j;
  7.    ...
  8.    i=k;
  9.    u=h-i+b;
  10.    ...
  11. }
复制代码
像上面代码中,编译器首先可以识别出i,j,k是循环变量,然后编译器发现h,u是循环变量的线性组合,
所以编译器可以识别出它们也是循环变量。(其中a,b可以是变量,只要在循环体内部没有被修改)
比如h每个循环改变的量为3*(a+1),u每个循环改变的量为3*a,编译器可以通过将上面代码改变为:
  1. i=0;
  2. h=3*a+1;
  3. u=3*a+1+b;
  4. step1=3*(a+1);
  5. step2=3*a;
  6. while(...){
  7.    j=i+1;
  8.    ...
  9.    k=j+2;
  10.    ///h=a*k+j;///删除原先这里对h的定义
  11.    ...
  12.    i=k;
  13.    ///u=h-i+b;///删除这里对u的定义
  14.    ...
  15.    h+=step1;
  16.    u+=step2;
  17. }
复制代码
在经过这个变换以后,在循环体内部, 所有关于循环变量的计算均可以不包含乘法运算,从而比原先代码应该可以快一些。
同样,如果在编译器优化比较后面的部分,通常,对于数组的访问都已经被展开,
如代码
  1.   for(i=0;i<n;i++){
  2.        a[i] =....;
  3.   }
复制代码
可能被展开成:
  1. for(i=0;i<n;i++){
  2.      p=a+i*S; ///这里S是常数,代表数组a中每个元素在内存中占用的空间大小
  3.      *p=...;
  4. }
复制代码
那么对于编译器来说,指针p也是一个循环变量,所以代码可以被转化为
  1. p=a;
  2. for(i=0;i<n;i++){
  3.      *p=...;
  4.      p=p+S;
  5. }
复制代码
变化以后同样计算地址中的乘法运算被消除了。
我看到郭给出链接中一篇英文文章中介绍到对于数组,最好让每个元素数据的大小是2的幂,这样,计算每个元素的地址时候,
乘法就可以被移位替换掉,从而提高了速度。但是,如果那样的数组通常都是被通过循环变量访问的,我们可以看出来,完全没有
必要做那样的优化(实际上那样可能会消耗更多的内存空间,从而性能更加差).
此外,有一些比较优秀的程序员,他们知道计算机计算移位比乘法运算快,所以对于下面的代码
  1. for(i=0;i<n;i++){
  2.    a[2*i]=...;
  3.    ...
  4. }
复制代码
他们可能写成了
  1. for(i=0;i<n;i++){
  2.    a[i<<1]=...;
  3.    ...
  4. }
复制代码
其实,对于编译器来说,反而前面的代码更加容易优化,因为编译器可以非常容易识别出2*i是一个循环变量,从而我们可以计算依赖向量,
做一些前面提到过的如么模变换,仿射变换之类的优化。反而对于后面的代码,由于通常编译器是不会将移位运算转化为乘法运算的,所以
通常的编译器反而无法知道后面的i<<1也是一个循环变量,从而阻止了进一步优化的可能。

此外,部分编译器还会对一些循环变量之间的相乘做优化(比如Open64),比如代码:
  1. i=0;
  2. while(...){
  3.    j=i+1;
  4.    ...
  5.    k=j+2;
  6.    h=a*k+j;
  7.    ...
  8.    i=k;
  9.    u=h-i+b;
  10.    ...
  11.    sum+=h*u;
  12. }
复制代码
在编译器分析出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;
所以我们知道,上面代码于是可以优化成:
  1. i=0;
  2. h=3*a+1;
  3. u=3*a+1+b;
  4. step1=3*(a+1);
  5. step2=3*a;
  6. hu=h*u;
  7. ddhu=2*step1*step2;
  8. dhu=h0*step2+u0*step1+step1*step2+ddhu*i;
  9. while(...){
  10.    j=i+1;
  11.    ...
  12.    k=j+2;
  13.    ///h=a*k+j;///删除原先这里对h的定义
  14.    ...
  15.    i=k;
  16.    ///u=h-i+b;///删除这里对u的定义
  17.    ...
  18.    h+=step1;
  19.    u+=step2;
  20.    sum+=hu;
  21.    hu+=dhu;
  22.    dhu+=ddhu;   
  23. }
复制代码
从而计算循环体内计算h*u的乘法运算由两次加法运算代替,从而提高了速度。
同样道理,对于三个循环变量的相乘,从理论上,我们同样可以转化为若干次加法运算。
不过据我所知,并没有编译器真正这样去做,毕竟实际中,这样代码的例子会非常少见。
当然,如果换成用郭的HugeCalc代码中大整数做循环变量的代码,那么遇到上面的代码编译器
的优化同样无能为力了,那么就需要手工做类似的优化了。

转载于:https://my.oschina.net/u/218425/blog/51947

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值