天天看點

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

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

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

繼續閱讀