其實大多數的編譯器本身就能提供一些簡單的優化,比如gcc就能通過使用 -O2 或者 -O3 的選項來優化程式。但編譯器的優化始終也是有限,因為它必須小心翼翼保證優化過程不對程式的功能有改動。故而程式設計師本身應該對程式有優化意識。在我看來,這也是應該有的一種良好的程式設計習慣。
幾種比較簡單的優化措施:
1.程式碼移動
將要執行多次(比如在迴圈中)但計算結果不會改變的計算,移動到程式碼前面不會多次求值的部分。舉一個比較極端的例子:
/* convert string to lowercase: slow*/ void lower( char *s ){ int i; for( i = 0;i < strlen(s);i++ ) if( s[i] >= 'A' && s[i] <= 'Z' ) s[i] -= ( 'A' - 'a');
}
因為C語言的字串是以null結尾的,函式strlen也必須一步一步得檢查這個序列,直到遇到null字元。那麼假象一下,如果字串s是一個很長的字串,那麼這個函式自然會造成許多不必要的開銷!!
故而在迴圈體內,要注意將計算結果不改變的計算移動到前面避免多次重複計算。
優化程式碼:
/* convert string to lowercase: faster*/
void lower( char *s ){
int i;
int len = strlen(s);
for( i = 0;i < len;i++ )
if( s[i] >= 'A' && s[i] <= 'Z' )
s[i] -= ( 'A' - 'a');
}
2.消除不必要的儲存器引用
在C語言中用指標變數讀寫是用CPU暫存器間接定址然後從記憶體中讀寫,而使用函式內部的區域性變數,則是使用CPU中的通用暫存器。而主存讀寫和CPU內部通用暫存器的定址的速度相差數十倍的。舉一個小例子
for( i = 0;i < len;i++ ){
*dest = *dest + data[i];
}
這個迴圈體每次都會從主存中讀寫,優化如下:
int acc;
for( i = 0;i < len;i++ ){
acc = acc + data[i];
}
*dest = acc;
這樣就會使那個指標只寫入一次,而acc變數在cpu的執行過程中是使用cpu內部通用暫存器讀寫,故而能加快速度。
3.迴圈展開
迴圈展開,顧名思義就是將一次一步的迭代迴圈展開成一次兩步或更多,減少迭代次數。迴圈展開從兩個方面改善程式的效能,首先,它減少了不直接有助於程式結果的操作的數量,比如迴圈索引計算和條件分支。其次,它提供了一些方法,可以進一步變化程式碼,減少計算中關鍵路徑上的運算元量。比較如下兩個函式,第一個為常規迴圈,第二個為迴圈展開函式,
//normal function to add all element of v
void combine1( vec_ptr v,data_t *dest ){ int i = 0; long int length = vec_length( v );
data_t *data = get_vec_start( v );
data_t acc = IDENT;
for( i = 0;i < length;i++ ){ acc = acc + data[i]; } *dest = acc; }
//unroll loop by 2
void combine2( vec_ptr v, data_t *dest ){ int i; long int length = vec_length( v ); loing int limit = length -1; data_t *data = get_vec_start( v ); data_t acc = IDENT; for( i = 0;i < limit;i += 2 ){ acc = ( acc + data[i] ) + data[i+1]; } for( ;i < length;i++ ){ acc = acc + data[i]; } *dest = acc; }
第二個函式將迴圈展開,並在最後檢查會不會遺漏。減少了一些關鍵步驟,故而優化了程式。
4.提高並行性
在cpu中,程式被翻譯成彙編指令,但卻並不是一條一條指令按順序執行的,而是流水線併發執行的,即多條不相關指令共同執行。這是cpu的機器特性,而我們要做的,就是多多利用這種機器特性。
讓我們來分析程式的combine2中的核心迴圈內部語句:acc = ( acc + data[i] ) + data[i+1];在這個迴圈中,data[i+1]的計算必須放在( acc + data[i] )之後,因為它們是相互關聯的,這明顯是不利於程式的並行操作,改進如下。
//unroll loop by 2,2-way parallelism
void combine3( vec_ptr v, data_t *dest ){
int i;
long int length = vec_length( v );
loing int limit = length -1;
data_t *data = get_vec_start( v );
data_t acc0 = IDENT;
data_t acc1 = IDENT;
for( i = 0;i < limit;i += 2 ){
acc0 = acc0 + data[i];
acc1 = acc1 + data[i+1];
}
for( ;i < length;i++ ){
acc0 = acc0 + data[i];
}
*dest = acc0 + acc1;
}
這段程式碼將acc拆分成acc0和acc1,使程式得以併發同時計算,最後再將兩組結果想加,提高程式效能。
程式碼優化通常都會帶來可讀性的降低,如何取捨應該好好考慮清楚,必要時刻,或許應該多加一些註釋說明。