以C語言為例的程式效能優化 --《深入理解計算機系統》第五章讀書筆記

zzzzMing發表於2016-09-09

  其實大多數的編譯器本身就能提供一些簡單的優化,比如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,使程式得以併發同時計算,最後再將兩組結果想加,提高程式效能。

 程式碼優化通常都會帶來可讀性的降低,如何取捨應該好好考慮清楚,必要時刻,或許應該多加一些註釋說明。

相關文章