程序優化的另一個出發點是減少運行過程中的運算量,有兩個大的思路:
1)把部分計算量轉移到離線,或者說把一部分工作挪到程序之外,人爲處理,以減輕程序本身壓力。比如查表、浮點轉定點以及其他數學算法的優化等。
2)分析和剔除代碼中的多餘水分,由於編譯器能把一些簡單的無效語句剔除,所以程序員可以做文章的地方一般就是循環體。
查表
有些算法輸入有限離散整數,輸出固定的數據集合,這種模塊本質只是提供一個有限數據集或者說常量數組,在程序裏現場計算完全是浪費CPU資源,完全可以事先算好,形成數據表放在內存數據區,運行中只要查表而無需計算,是一種空間換時間的策略。如:
long factorial(int i)
{
if (i == 0) { return 1; }
else { return i * factorial(i - 1); }
}
新代碼:
static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720 /* etc */ };
long factorial(int i)
{
return factorial_table[i];
}
浮點轉定點
很多CPU沒有專門針對浮點運算的硬件,而是通過軟件庫模擬浮點運算,效率非常低。如果程序中有大量浮點運算,應該用定點替代。基本原理是,確定某運算模塊的浮點輸入數據集的範圍,在此基礎上縮放2Q,把浮點數縮放爲定點整數,然後進行整數運算,之後再把結果重新轉換回浮點。簡單說:
float Mod1(float x, float y)
{
float z;
//系列x,y的浮點運算,得到結果z
return z;
}
假設x,y 的數據集範圍都在[0.125,1 ],函數可大體改成
float Mod1(float x, float y)
{
int ix =(int)x*8;
int iy=(int)y*8;
int iz;
//系列 ix,iy的整數運算得到結果iz
return (float )iz/float(8*8);
}
這一過程的原理在DSP資料中有詳細論述,關鍵在於爲不同模塊的浮點運算確定合適的小數點變換位置,使變換後的運算既不溢出整數範圍,又能保持足夠精度,即所謂合理定標。這部分內容大家可自行查閱參考相關資料☺。
循環優化
循環往往是profiler工具標識的程序計算集中的“熱點”,因而是軟件優化的重點對象,着眼點包括:
a. 優化計數器訪問
C的while /for循環中都需要一個計數器(counter),這個counter最好設計成遞減到零,而不要用遞增計數,兩個原因:一是與0做比較的判斷指令在某些芯片(ARM)中附加在算術運算指令之後,合2條指令爲1條,這樣每次遞減循環少用一條判斷指令(參考ARM彙編);二是遞減循環不必保存counter的最大極限值,如果這個極限值是一個變量,那麼用遞增計數就要多佔用一個寄存器。
b.把循環內的重複運算提取到循環之外
仔細觀察,把循環內部確定的分支或計算提到循環外,以消除重複,如:
for(i=0; i<MAX; i++){
if (n== 0) a(i) = a(i) + b(i) * c ;
else a(i) = 0;
}
這裏n是否爲0的判斷和循環內其它運算沒有關係,沒必要每次重複判斷,可改爲:
if (n == 0) {
for(i = 0;i < MAX; i++) a(i) = a(i) + b(i) * c;
}else{
for(i= 0;i <k; i++) a(i) = 0 ;
}
代碼似乎多了,但執行效率要高,再比如:
int GetCRC(char *instr)
{
int a;
int x = -1;
for(a = 0; a<strlen(instr);a++) { x+=*(int*)((int)instr+a); }
return x;
}
strlen是一個函數,編譯器根據已有條件並不知道strlen結果始終不變,所以會笨笨的重複計算。很明顯應增加一個局部變量int b,在for循環前計算b=strlen(instr),把循環變爲for(a=0;a<b;a++)。消除這類重複計算,既提升代碼的質量,也爲低碳環保,綠色地球做出了貢獻:)
c.循環展開
循環展開可以減小循環次數,降低循環判斷的開銷。如: for(i=0; i<100; i++) { temp = temp*(array[i]); }
這個循環內部運算很簡單,但每次i<100的判斷必不可少,這樣循環中很大比例的指令消耗在循環結束條件的判斷上,因此可以展開這個循環,變爲:
temp = temp*(array[0]);
......
temp = temp*(array[99]);
這樣展開雖然看上去把原來兩句話增加到100句,但實際可以減少100條判斷指令。
再舉例有一幅24位真色彩圖像,每像素包含R、G和B三分量,RGB分量各佔8位,在0~255間取值。下面函數實現圖像取反色,即每像素每分量都被255減。
void NegPixel(Uint8* InPixel, Uint8 *OutPixel, int Width, int Height)
{
int sum = Width * Height * 3;
for(int i=0;i<sum;i++) { OutPixel[i]=255-InPixel[i]; }
}
循環體中的i<sum判斷佔了相當比例,於是展開:
void NegPixel (Uint8 * InPixel, Uint8 * OutPixel,int Width, int Height)
{
int sum = Width * Height ;
for(int i =0;i<sum;i+=3)
{
OutPixel[i]=255-InPixel[i];
OutPixel[i+1]=255-InPixel[i+1];
OutPixel[i+2]=255-InPixel[i+2];
}
}
循環次數變爲原來三分之一,減少了2*sum/3個條件判斷指令,這種部分展開能兼顧優化和代碼密度。那如果循環次數是變量或某素數,怎麼部分展開呢?首先確保第一次循環不超過數組邊界,如原先循環長度爲n,展開3次,可將循環限制設爲n-2,使循環體內數組最大索引不超過數組長度n。如:
void function(int array[],int *dest, int len)
{
int temp=1;
for(int i=0;i<len;i++){ temp=temp*(array[i]); }
*dest = temp;
}
展開爲
void function(int array[],int *dest, int len)
{
int temp=1;
int limit=len-2;
for(int i=0;i<limit;i+=3){ temp=temp*(array[i])*(array[i+1])*(array[i+2]); }
for(; i < len; i++){ temp = temp * array[i]; }
*dest = temp;
}
所以如果循環展開k次,就把上限設爲n-k+1,最大循環索引將會比n小,最後再處理掉餘下的最後幾個元素。
不過天下沒有免費的午餐,循環展開固然能降低分支判斷的開銷,卻會增加代碼量,所以最優展開位置要具體分析。且還要注意,在有指令cache的CPU上,如果循環展開後代碼超出cache line,展開代碼可能導致cache miss,因此這時展開循環有可能反而變慢。另外一些編譯器在優化級別足夠高時會自動展開循環。所以是否要手工展開也要具體情況具體分析。