2016年1月6日 星期三

高效率C語言設計

這篇文章只著重在C語言反組譯後的效能,並不探討架構。這是個冷門知識,除了無線通訊的核心程式、高速介面的核心程式外,很少有人會在乎。探討這類的書也不多,通常也很古老,最慘的是部分守則在GCC的進步下,已經沒有用了。本篇文章就一一檢視這些守則。書本用的是"ARM System Developer's Guide - Designing and Optimizing System Software"




  • 局部變數

    • 過時守則 : 32bit CPU上,不要使用char當作區域變數,要改用unsigned int 

int checksum_v1 (int *data) {
        char i;
        int sum = 0;
        for (i=0; i<64; i++) {
                sum +=data[i];
        }   

        return sum;
}

int checksum_v2 (int *data) {
        unsigned int i;
        int sum = 0;
        for (i=0; i<64; i++) {
                sum +=data[i];
        }   

        return sum;
}


理由:
     register寬度為32bit。如果變數是char,編譯器反而要register跟 0xFF 做mask,這樣才能保證不會溢位。且unsigned可以熱

實驗結果: 
      checksum_v1()和checksum_v2() 並沒差,不需要改變。

    • 守則 : 不能把 int加法的值存在short內,即便知道 short不會overflow

short checksum_v3 (int *data) {
        unsigned int i;
        short sum = 0;
        for (i=0; i<64; i++) {
                sum = (short) ( sum +data[i]);
        }   

        return sum;
}

short checksum_v4 (short *data) {
        unsigned int i;
        int sum = 0;
        for (i=0; i <64; i++) {
                sum += *(data ++);
        }
        return (short) sum;
}

理由 :
    compiler會多花幾個arm 指令把值塞入 short內

實驗結果: 
     在checksum_v3() 的迴圈內會多花一個指令 (uxth - zero extend halfword. Extend 16-bit value to 32-bit value)去把short先變成 int。離開迴圈後,再用另一個指令 (sxth - sign extend halfword) 去把 int變為short。

    • 守則 : 使用除法時,盡量使用無符號數

int average_v1 ( int a, int b) {
        return (a+b) /2
}

理由: 
    對負數除二時,需要先加一然後再往右shift一位。ex:  -3/2 = -1 然而 -3 >> 1 = -2,所以需要先加一然後再移位。

實驗結果:
     依然有效

  • C迴圈結構
    • 過時守則 : For Loop內要使用index時,要用遞減方式

int checksum_v5 (int *data) {
        unsigned int i;
        int sum = 0;
        for ( i = 0; i< 64;i++) {
                sum += *(data++);
        }   
        return sum;
}


int checksum_v6 (int *data) {
        unsigned int i;
        int sum = 0;
        for( i=64; i!=0; i--) {
                sum+=*(data++);
        }   
        return sum;
}

理由 : 
    當時的GCC會多花一個指令處理遞增For Loop的index 。

實驗結果 :
    不需使用遞減方式,現在的GCC已經最佳化了,不需要考慮這個。

    • 守則 : 使用 do {} while ()替代 for loop

int checksum_v7 (int *data, unsigned int N) {
        int sum;
        for(; N!=0; N--) {
                sum += *(data++);
        }
        return sum;
}

int checksum_v8 (int *data, unsigned int N) {
        int sum;
        do {
                sum += *(data++);
        } while( --N !=0);
        return sum;
}

理由 : 
    For Loop在第一次迴圈時,會檢查index N是否為 0,但是通常N 不為 0,所以會多花一個arm 指令。 

實驗結果 :
    仍然有效

  • 迴圈展開
    • 守則 : 把迴圈內的內容展開,如此可以減少判斷終止的次數

int checksum_v9 (int *data, unsigned int N ) { 
    int sum = 0;
    do {
        sum += *(data++);
        sum += *(data++);
        sum += *(data++);
        sum += *(data++);
        N -= 4;
    } while (N != 0); 
    return sum;
}


理由 :
     當迴圈的指令數很少時,判斷中止條件的指令就佔很大的比例。將迴圈的內容展開,可以稀釋中止條件所佔的比例。

實驗結果 :
    仍然有效