循環變量的優化

如今來介紹編譯器如何識別循環變量的問題。
好比前面各類關於循環的優化中,咱們若是要計算依賴向量,起碼編譯器要可以識別循環變量,
而識別循環變量也是編譯器的一個優化過程。
而一般編譯器所認爲的循環變量可能同咱們所看到的有所不一樣。
一般的循環變量是狹義的,也就是說,若是這個變量在一個循環的任意兩次相鄰的迭代中變換值是常數,
那麼這個變量就是循環變量。最多見的循環變量是以下代碼中:
  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代碼中大整數作循環變量的代碼,那麼遇到上面的代碼編譯器
的優化一樣無能爲力了,那麼就須要手工作相似的優化了。
相關文章
相關標籤/搜索