其實大多數的編譯器本身就能提供一些簡單的優化,比如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,使程式得以并發同時計算,最後再将兩組結果想加,提高程式性能。
代碼優化通常都會帶來可讀性的降低,如何取舍應該好好考慮清楚,必要時刻,或許應該多加一些注釋說明。