C優化篇之減少運算量

    程序優化的另一個出發點是減少運行過程中的運算量,有兩個大的思路:

    1)把部分計算量轉移到離線,或者說把一部分工作挪到程序之外,人爲處理,以減輕程序本身壓力。比查表、浮點轉定點以及其他數學算法的優化等。

    2)分析和剔除代碼中的多餘水分,由於編譯器能把一些簡單的無效語句剔除,所以程序員可以做文章的地方一般就是循環體。

查表

   有些算法輸入有限離散整數,輸出固定的數據集合,這種模塊本質只是提供一個有限數據集或者說常量數組,在程序裏現場計算完全是浪費CPU資源,完全可以事先算好,形成數據表放在內存數據區,運行中只要查表而無需計算,是一種空間換時間的策略。如:

    long factorial(int i)

    {

      if (i == 0) {  return 1;  }

      else {  return  i * factorial(i - 1);  }

    }

    新代碼:

    static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720  /* etc */ };

    long factorial(int i)

    {

      return factorial_table[i];

    }

浮點轉定點

    很多CPU沒有專門針對浮點運算的硬件,而是通過軟件庫模擬浮點運算,效率非常低。如果程序中有大量浮點運算,應該用定點替代。基本原理是,確定某運算模塊的浮點輸入數據集的範圍,在此基礎上縮放2Q,把浮點數縮放爲定點整數,然後進行整數運算,之後再把結果重新轉換回浮點。簡單說:

    float Mod1(float x, float y) 

    {

       float z;

        //系列x,y的浮點運算,得到結果

       return z;

    }

    假設x,y 的數據集範圍都在[0.125,1 ],函數可大體改成

    float Mod1(float x, float y)

    {

       int ix =(int)x*8;

       int iy=(int)y*8;

       int iz;

       //系列 ix,iy的整數運算得到結果iz

      return (float )iz/float(8*8);

    }

    這一過程的原理在DSP資料中有詳細論述,關鍵在於爲不同模塊的浮點運算確定合適的小數點變換位置,使變換後的運算既不溢出整數範圍,又能保持足夠精度,即所謂合理定標。這部分內容大家可自行查閱參考相關資料

循環優化

    循環往往是profiler工具標識的程序計算集中的“熱點”,因而是軟件優化的重點對象,着眼點包括:

a. 優化計數器訪問

    Cwhile /for循環中都需要一個計數器(counter),這個counter最好設計成遞減到零,而不要用遞增計數,兩個原因:一是與0做比較的判斷指令在某些芯片(ARM)中附加在算術運算指令之後,合2條指令爲1條,這樣每次遞減循環少用一條判斷指令(參考ARM彙編);二是遞減循環不必保存counter的最大極限值,如果這個極限值是一個變量,那麼用遞增計數就要多佔用一個寄存器。

b.把循環內的重複運算提取到循環之外

    仔細觀察,把循環內部確定的分支或計算提到循環外,以消除重複,如:

    for(i=0; i<MAX; i++){

      if (n== 0)    a(i) = a(i) + b(i) * c ;

      else    a(i) = 0;

    }

    這裏n是否爲0的判斷和循環內其它運算沒有關係,沒必要每次重複判斷,可改爲:

    if (n == 0) {

      for(i = 0;i < MAX; i++)  a(i) = a(i) + b(i) * c;

    }else{

      for(i= 0;i <k; i++)   a(i) = 0 ;

    }

    代碼似乎多了,但執行效率要高,再比如:

    int GetCRC(char *instr)

    {

      int a;

      int x = -1;

      for(a = 0; a<strlen(instr);a++)    {      x+=*(int*)((int)instr+a);      }

      return x;

    }

    strlen是一個函數,編譯器根據已有條件並不知道strlen結果始終不變,所以會笨笨的重複計算。很明顯應增加一個局部變量int b,在for循環前計算b=strlen(instr),把循環變爲for(a=0;a<b;a++)消除這類重複計算,既提升代碼的質量,也爲低碳環保,綠色地球做出了貢獻:)

c.循環展開

    循環展開可以減小循環次數降低循環判斷的開銷。如    for(i=0; i<100; i++) {   temp = temp*(array[i]);  }

   這個循環內部運算很簡單但每次i<100的判斷必不可少這樣循環中很大比例的指令消耗在循環結束條件的判斷上,因此可以展開這個循環,變爲:

    temp = temp*(array[0]);

    ......

    temp = temp*(array[99]);

    這樣展開雖然看上去把原來兩句話增加到100句,但實際可以減少100條判斷指令。

    再舉例有一幅24位真色彩圖像,每像素包含RGB三分量,RGB分量各佔8位,0255間取值。下面函數實現圖像取反色,即每像素每分量都被255減。

    void NegPixel(Uint8* InPixel, Uint8 *OutPixel, int Width, int Height)

    {

      int sum = Width * Height * 3;

      for(int i=0;i<sum;i++)  {   OutPixel[i]=255-InPixel[i];   }

    }

   循環體中的i<sum判斷佔了相當比例,於是展開:

    void NegPixel (Uint8 * InPixel, Uint8 * OutPixel,int Width, int Height)

    {

      int sum = Width * Height ;

      for(int i =0;i<sum;i+=3)

      {

        OutPixel[i]=255-InPixel[i];

        OutPixel[i+1]=255-InPixel[i+1];

        OutPixel[i+2]=255-InPixel[i+2];

      }

    }

    循環次數變爲原來三分之減少了2*sum/3個條件判斷指令,這種部分展開能兼顧優化和代碼密度。那如果循環次數是變量或某素數,怎麼部分展開呢?首先確保第一次循環不超過數組邊界,如原先循環長度爲n,展開3次,可將循環限制設爲n-2,使循環體內數組最大索引不超過數組長度n。如:

    void function(int array[],int *dest, int len)

    {

      int temp=1;

      for(int i=0;i<len;i++){        temp=temp*(array[i]);       }

      *dest = temp;

    }

    展開爲

    void function(int array[],int *dest, int len)

    {

      int temp=1;

      int limit=len-2;

      for(int i=0;i<limit;i+=3){    temp=temp*(array[i])*(array[i+1])*(array[i+2]);     }

      for(; i < len; i++){    temp = temp * array[i];     }

      *dest = temp;

    }

    所以如果循環展開k就把上限設爲n-k+1最大循環索引將會比n小,最後再處理掉餘下的最後幾個元素。

    不過天下沒有免費的午餐,循環展開固然能降低分支判斷的開銷,卻會增加代碼量,所以最優展開位置要具體分析。且還要注意,在有指令cacheCPU上,如果循環展開後代碼超出cache line,展開代碼可能導致cache miss,因此這時展開循環有可能反而變慢。另外一些編譯器在優化級別足夠高時會自動展開循環。所以是否要手工展開也要具體情況具體分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章