在九十年代末時,我一個同學在寫一個處理醫學映像的程式,裡面用了不少三角函數 ,所以程式運行很慢(那時機器也慢,可能主頻都在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