c語言中的代碼優化《轉》

在性能優化方面永遠注意80-20原則,即20%的程序消耗了80%的運行時間,於是咱們要改進效率,最主要是考慮改進那20%的代碼。不要優化程序中開銷不大的那80%,這是勞而無功的。

第一招:以空間換時間

  計算機程序中最大的矛盾是空間和時間的矛盾,那麼,從這個角度出發逆向思惟來考慮程序的效率問題,咱們就有了解決問題的第1招--以空間換時間。好比說字符串的賦值:

方法A:一般的辦法

#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,"This is a example!!");

方法B:

const char string2[LEN] ="This is a example!";
char * cp;
cp = string2

使用的時候能夠直接用指針來操做。

從上面的例子能夠看出,A和B的效率是不能比的。在一樣的存儲空間下,B直接使用指針就能夠操做了,而A須要調用兩個字符函數才能完成。B的缺點在於靈活 性沒有A好。在須要頻繁更改一個字符串內容的時候,A具備更好的靈活性;若是採用方法B,則須要預存許多字符串,雖然佔用了大量的內存,可是得到了程序執 行的高效率。

若是系統的實時性要求很高,內存還有一些,那我推薦你使用該招數。

第二招: 使用宏而不是函數。

  這也是第一招的變招。函數和宏的區別就在於,宏佔用了大量的空間,而函數佔用了時間。你們要知道的是,函數調用是要使用系統的棧來保存數據的,若是編 譯器裏有棧檢查選項,通常在函數的頭會嵌入一些彙編語句對當前棧進行檢查;同時,CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操做,所 以,函數調用須要一些CPU時間。而宏不存在這個問題。宏僅僅做爲預先寫好的代碼嵌入到當前程序,不會產生函數調用,因此僅僅是佔用了空間,在頻繁調用同 一個宏的時候,該現象尤爲突出。

舉例以下:

方法C:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK(int __bf)
{
 return ((1U << (bw ## __bf)) - 1)<< (bs ## __bf);
}
void SET_BITS(int __dst,
int __bf, int __val)
{
 __dst = ((__dst) & ~(BIT_MASK(__bf))) |
\
 (((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
}
SET_BITS(MCDR2, MCDR2_ADDRESS,ReGISterNumber);

方法D:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)
#define BIT_MASK(__bf)
(((1U << (bw ## __bf)) - 1)
<< (bs ## __bf))
#define SET_BITS(__dst, __bf, __val)
\
((__dst) = ((__dst) & ~(BIT_MASK(__bf)))
| \
(((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS,
RegisterNumber);

D方法是我看到的最好的置位操做函數,是ARM公司源碼的一部分,在短短的三行內實現了不少功能,幾乎涵蓋了全部的位操做功能。C方法是其變體,其中滋味還需你們仔細體會。

第三招:數學方法解決問題

  如今咱們演繹高效C語言編寫的第二招--採用數學方法來解決問題。數學是計算機之母,沒有數學的依據和基礎,就沒有計算機的發展,因此在編寫程序的時候,採用一些數學方法會對程序的執行效率有數量級的提升。舉例以下,求 1~100的和。

方法E:

int I , j;
for (I = 1 I<=100; I ++)
{
 j += I;
}

方法F

int I;
I = (100 * (1+100)) / 2

這個例子是我印象最深的一個數學用例,是個人計算機啓蒙老師考個人。當時我只有小學三年級,惋惜我當時不知道用公式 N×(N+1)/ 2 來解決這個問題。方法E循環了100次才解決問題,也就是說最少用了100個賦值,100個判斷,200個加法(I和j);而方法F僅僅用了1個加法,1 次乘法,1次除法。效果天然不言而喻。因此,如今我在編程序的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提升程序運行的效率。
第四招:使用位操做

  使用位操做。減小除法和取模的運算。在計算機程序中數據的位是能夠操做的最小數據單位,理論上能夠用"位運算"來完成全部的運算和操做。通常的位操做是用來控制硬件的,或者作數據變換使用,可是,靈活的位操做能夠有效地提升程序運行的效率。舉例以下:

方法G

int I,J;
I = 257 /8;
J = 456 % 32;

方法H

int I,J;
I = 257 >>3;
J = 456 - (456 >> 4 << 4);

在字面上好像H比G麻煩了好多,可是,仔細查看產生的彙編代碼就會明白,方法G調用了基本的取模函數和除法函數,既有函數調用,還有不少彙編代碼和寄存器 參與運算;而方法H則僅僅是幾句相關的彙編,代碼更簡潔,效率更高。固然,因爲編譯器的不一樣,可能效率的差距不大,可是,以我目前遇到的MS C ,ARM C 來看,效率的差距仍是不小。對於以2的指數次方爲"*"、"/"或"%"因子的數學算,轉爲移位運算"<< >>"一般能夠提升算法效率。由於乘除運算指令週期一般比移位運算大。C語言位運算除了能夠提升運算效率外,在嵌入式系統的編程中,它的另外一 個最典型的應用,並且十分普遍地正在被使用着的是位間的與(&)、或(|)、非(~)操做,這跟嵌入式系統的編程特色有很大關係。咱們一般要對硬 件寄存器進行位設置,譬如,咱們經過將AM186ER型80186處理器的中斷屏蔽控制寄存器的第低6位設置爲0(開中斷2),最通用的作法是:


#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);   

而將該位設置爲1的作法是:

#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);   

判斷該位是否爲1的作法是:

#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp & INT_I2_MASK)
{
… /* 該位爲1 */
}   

運用這招須要注意的是,由於CPU的不一樣而產生的問題。好比說,在PC上用這招編寫的程序,並在PC上調試經過,在移植到一個16位機平臺上的時候,可能會產生代碼隱患。因此只有在必定技術進階的基礎下才可使用這招。

  

第五招:彙編嵌入

  在熟悉彙編語言的人眼裏,C語言編寫的程序都是垃圾"。這種說法雖然偏激了一些,可是卻有它的道理。彙編語言是效率最高的計算機語言,可是,不可能靠 着它來寫一個操做系統吧?因此,爲了得到程序的高效率,咱們只好採用變通的方法--嵌入彙編,混合編程。嵌入式C程序中主要使用在線彙編,即在C程序中直 接插入_asm{ }內嵌彙編語句。
舉例以下,將數組一賦值給數組二,要求每一字節都相符。
char string1[1024],string2[1024];

方法I

int I;
for (I =0 I<1024;I++)
 *(string2 + I) = *(string1 + I)

方法J

#ifdef _PC_
int I;
for (I =0 I<1024;I++)
*(string2 + I) = *(string1 + I);
#else
#ifdef _ARM_
__asm
{
 MOV R0,string1
 MOV R1,string2
 MOV R2,#0
loop:
 LDMIA R0!, [R3-R11]
 STMIA R1!, [R3-R11]
 ADD R2,R2,#8
 CMP R2, #400
 BNE loop
}
#endif

再舉個例子:

/* 把兩個輸入參數的值相加,結果存放到另一個全局變量中 */
int result;
void Add(long a, long *b)
{
 _asm
 {
  MOV AX, a
  MOV BX, b
  ADD AX, [BX]
  MOV result, AX
 }
}

方法I是最多見的方法,使用了1024次循環;方法J則根據平臺不一樣作了區分,在ARM平臺下,用嵌入彙編僅用128次循環就完成了一樣的操做。這裏有朋 友會說,爲何不用標準的內存拷貝函數呢?這是由於在源數據裏可能含有數據爲0的字節,這樣的話,標準庫函數會提早結束而不會完成咱們要求的操做。這個例 程典型應用於LCD數據的拷貝過程。根據不一樣的CPU,熟練使用相應的嵌入彙編,能夠大大提升程序執行的效率。

雖然是必殺技,可是若是輕易使用會付出慘重的代價。這是由於,使用了嵌入彙編,便限制了程序的可移植性,使程序在不一樣平臺移植的過程當中,臥虎藏龍,險象環生!同時該招數也與現代軟件工程的思想相違背,只有在無可奈何的狀況下才能夠採用。

第六招, 使用寄存器變量
    當對一個變量頻繁被讀寫時,須要反覆訪問內存,從而花費大量的存取時間。爲此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使 用時,不須要訪問內存,而直接從寄存器中讀寫,從而提升效率。寄存器變量的說明符是register。對於循環次數較多的循環控制變量及循環體內反覆使用 的變量都可定義爲寄存器變量,而循環計數是應用寄存器變量的最好候選者。

  (1) 只有局部自動變量和形參才能夠定義爲寄存器變量。由於寄存器變量屬於動態存儲方式,凡須要採用靜態存儲方式的量都不能定義爲寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部static變量;

  (2) register是一個"建議"型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能由於條件不知足並未成爲寄存器變量,而是被放在了存儲器中,但編譯器中並不報錯(在C++語言中有另外一個"建議"型關鍵字:inline)。

  下面是一個採用寄存器變量的例子:

/* 求1+2+3+….+n的值 */

WORD Addition(BYTE n)
{
 register i,s=0;
 for(i=1;i<=n;i++)
 {
  s=s+i;
 }
 return s;
}   


  本程序循環n次,i和s都被頻繁使用,所以可定義爲寄存器變量。

第七招: 利用硬件特性

  首先要明白CPU對各類存儲器的訪問速度,基本上是:

CPU內部RAM > 外部同步RAM > 外部異步RAM > FLASH/ROM

  對於程序代碼,已經被燒錄在FLASH或ROM中,咱們可讓CPU直接從其中讀取代碼執行,但一般這不是一個好辦法,咱們最好在系統啓動後將FLASH或ROM中的目標代碼拷貝入RAM中後再執行以提升取指令速度;

  對於UART等設備,其內部有必定容量的接收BUFFER,咱們應儘可能在BUFFER被佔滿後再向CPU提出中斷。例如計算機終端在向目標機經過RS-232傳遞數據時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;

  若是對某設備能採起DMA方式讀取,就採用DMA讀取,DMA讀取方式在讀取目標中包含的存儲信息較大時效率較高,其數據傳輸的基本單位是塊,而所傳 輸的數據是從設備直接送入內存的(或者相反)。DMA方式較之中斷驅動方式,減小了CPU 對外設的干預,進一步提升了CPU與外設的並行操做程度。程序員

 

C代碼優化方案算法

1、選擇合適的算法和數據結構編程

選擇一種合適的數據結構很重要,若是在一堆隨機存放的數中使用了大量的插入和刪除指令,那使用鏈表要快得多。數組與指針語句具備十分密切的關係,通常來講,指針比較靈活簡潔,而數組則比較直觀,容易理解。對於大部分的編譯器,使用指針比使用數組生成的代碼更短,執行效率更高。數組

在許多種狀況下,能夠用指針運算代替數組索引,這樣作經常能產生又快又短的代碼。與數組索引相比,指針通常能使代碼速度更快,佔用空間更少。使用多維數組時差別更明顯。下面的代碼做用是相同的,可是效率不同?緩存

    數組索引                  指針運算性能優化

      For(;;){                  p=array數據結構

      A=array[t++];          for(;;){異步

                                  a=*(p++);模塊化

    。。。。。。。。。                    。。。。。。函數

      }                        }

指針方法的優勢是,array的地址每次裝入地址p後,在每次循環中只需對p增量操做。在數組索引方法中,每次循環中都必須根據t值求數組下標的複雜運算。

2、使用盡可能小的數據類型

可以使用字符型(char)定義的變量,就不要使用整型(int)變量來定義;可以使用整型變量定義的變量就不要用長整型(long int),能不使用浮點型(float)變量就不要使用浮點型變量。固然,在定義變量後不要超過變量的做用範圍,若是超過變量的範圍賦值,C編譯器並不報錯,但程序運行結果卻錯了,並且這樣的錯誤很難發現。

在ICCAVR中,能夠在Options中設定使用printf參數,儘可能使用基本型參數(%c、%d、%x、%X、%u和%s格式說明符),少用長整型參數(%ld、%lu、%lx和%lX格式說明符),至於浮點型的參數(%f)則儘可能不要使用,其它C編譯器也同樣。在其它條件不變的狀況下,使用%f參數,會使生成的代碼的數量增長不少,執行速度下降。

3、減小運算的強度

1)、查表(遊戲程序員必修課)

一個聰明的遊戲大蝦,基本上不會在本身的主循環裏搞什麼運算工做,絕對是先計算好了,再到循環裏查表。看下面的例子:

舊代碼:

      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];

      }

若是表很大,很差寫,就寫一個init函數,在循環外臨時生成表格。

2)、求餘運算

      a=a%8;

能夠改成:

      a=a&7;

說明:位操做只需一個指令週期便可完成,而大部分的C編譯器的「%」運算均是調用子程序來完成,代碼長、執行速度慢。一般,只要求是求2n方的餘數,都可使用位操做的方法來代替。

3)、平方運算

a=pow(a, 2.0);

能夠改成:

a=a*a;

說明:在有內置硬件乘法器的單片機中(如51系列),乘法運算比求平方運算快得多,由於浮點數的求平方是經過調用子程序來實現的,在自帶硬件乘法器的AVR單片機中,如ATMega163中,乘法運算只需2個時鐘週期就能夠完成。既使是在沒有內置硬件乘法器的AVR單片機中,乘法運算的子程序比平方運算的子程序代碼短,執行速度快。

若是是求3次方,如:

a=pow(a,3。0);

更改成:

a=a*a*a;

則效率的改善更明顯。

4)、用移位實現乘除法運算

      a=a*4;

      b=b/4;

能夠改成:

      a=a<<2;

      b=b>>2;

一般若是須要乘以或除以2n,均可以用移位的方法代替。在ICCAVR中,若是乘以2n,均可以生成左移的代碼,而乘以其它的整數或除以任何數,均調用乘除法子程序。用移位的方法獲得代碼比調用乘除法子程序生成的代碼效率高。實際上,只要是乘以或除以一個整數,都可以用移位的方法獲得結果,如:

      a=a*9

能夠改成:

a=(a<<3)+a

採用運算量更小的表達式替換原來的表達式,下面是一個經典例子:

舊代碼:

      x = w % 8;

      y = pow(x, 2.0);

      z = y * 33;

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

      {

          h = 14 * i;

          printf("%d", h);

      }

新代碼:

      x = w &   7;                /* 位操做比求餘運算快*/

      y = x * x;                   /* 乘法比平方運算快*/

      z = (y << 5) + y;          /* 位移乘法比乘法快 */

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

      {

          h +=   14;                  /* 加法比乘法快 */

          printf("%d",h);

}

5)、避免沒必要要的整數除法

整數除法是整數運算中最慢的,因此應該儘量避免。一種可能減小整數除法的地方是連除,這裏除法能夠由乘法代替。這個替換的反作用是有可能在算乘積時會溢出,因此只能在必定範圍的除法中使用。

很差的代碼:

int i, j, k, m;

m = i / j / k;

推薦的代碼:

int i, j, k, m;

m = i / (j * k);

6)、使用增量和減量操做符

在使用到加一和減一操做時儘可能使用增量和減量操做符,由於增量符語句比賦值語句更快,緣由在於對大多數CPU來講,對內存字的增、減量操做沒必要明顯地使用取內存和寫內存的指令,好比下面這條語句:

x=x+1;

模仿大多數微機彙編語言爲例,產生的代碼相似於:

move A,x      ;把x從內存取出存入累加器A

add A,1          ;累加器A加1

store   x          ;把新值存回x

若是使用增量操做符,生成的代碼以下:

incr   x           ;x加1

顯然,不用取指令和存指令,增、減量操做執行的速度加快,同時長度也縮短了。

7)、使用複合賦值表達式

複合賦值表達式(如a-=1及a+=1等)都可以生成高質量的程序代碼。

8)、提取公共的子表達式

在某些狀況下,C++編譯器不能從浮點表達式中提出公共的子表達式,由於這意味着至關於對錶達式從新排序。須要特別指出的是,編譯器在提取公共子表達式前不能按照代數的等價關係從新安排表達式。這時,程序員要手動地提出公共的子表達式(在VC.NET裏有一項「全局優化」選項能夠完成此工做,但效果就不得而知了)。

很差的代碼:

float a, b, c, d, e, f;

。。。

e = b * c / d;

f = b / d * a;

推薦的代碼:

float a, b, c, d, e, f;

。。。

const float t(b / d)

e = c * t;

f = a * t;

很差的代碼:

float a, b, c, e, f;

。。。

e = a / c;

f = b / c;

推薦的代碼:

float a, b, c, e, f;

。。。

const float t(1.0f / c)

e = a * t;

f = b * t;

4、結構體成員的佈局

不少編譯器有「使結構體字,雙字或四字對齊」的選項。可是,仍是須要改善結構體成員的對齊,有些編譯器可能分配給結構體成員空間的順序與他們聲明的不一樣。可是,有些編譯器並不提供這些功能,或者效果很差。因此,要在付出最少代價的狀況下實現最好的結構體和結構體成員對齊,建議採起下列方法:

1)按數據類型的長度排序

把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。編譯器要求把長型數據類型存放在偶數地址邊界。在申明一個複雜的數據類型 (既有多字節數據又有單字節數據) 時,應該首先存放多字節數據,而後再存放單字節數據,這樣能夠避免內存的空洞。編譯器自動地把結構的實例對齊在內存的偶數邊界。

2)把結構體填充成最長類型長度的整倍數

把結構體填充成最長類型長度的整倍數。照這樣,若是結構體的第一個成員對齊了,全部整個結構體天然也就對齊了。下面的例子演示瞭如何對結構體成員進行從新排序:

很差的代碼,普通順序:

struct

{

char a[5];

long k;

double x;

} baz;

推薦的代碼,新的順序並手動填充了幾個字節:

struct

{

double x;

long k;

char a[5];

char pad[7]

} baz;

這個規則一樣適用於類的成員的佈局。

3)按數據類型的長度排序本地變量

當編譯器分配給本地變量空間時,它們的順序和它們在源代碼中聲明的順序同樣,和上一條規則同樣,應該把長的變量放在短的變量前面。若是第一個變量對齊了,其它變量就會連續的存放,並且不用填充字節天然就會對齊。有些編譯器在分配變量時不會自動改變變量順序,有些編譯器不能產生4字節對齊的棧,因此4字節可能不對齊。下面這個例子演示了本地變量聲明的從新排序:

很差的代碼,普通順序

short ga, gu, gi;

long foo, bar;

double x, y, z[3];

char a, b;

float baz;

推薦的代碼,改進的順序

double z[3];

double x, y;

long foo, bar;

float baz;

short ga, gu, gi; 

4)把頻繁使用的指針型參數拷貝到本地變量

避免在函數中頻繁使用指針型參數指向的值。由於編譯器不知道指針之間是否存在衝突,因此指針型參數每每不能被編譯器優化。這樣數據不能被存放在寄存器中,並且明顯地佔用了內存帶寬。注意,不少編譯器有「假設不衝突」優化開關(在VC裏必須手動添加編譯器命令行/Oa或/Ow),這容許編譯器假設兩個不一樣的指針老是有不一樣的內容,這樣就不用把指針型參數保存到本地變量。不然,請在函數一開始把指針指向的數據保存到本地變量。若是須要的話,在函數結束前拷貝回去。

很差的代碼:

// 假設 q != r

void isqrt(unsigned   long a, unsigned long* q, unsigned long* r)

{

  *q = a;

  if (a > 0)

  {

    while (*q > (*r   = a / *q))

    {

      *q = (*q + *r)   >> 1;

    }

  }

  *r = a - *q * *q;

}

推薦的代碼:

// 假設 q != r

void isqrt(unsigned   long a, unsigned long* q, unsigned long* r)

{

  unsigned long qq, rr;

  qq = a;

  if (a > 0)

  {

    while (qq > (rr   = a / qq))

    {

      qq = (qq + rr)   >> 1;

    }

  }

  rr = a - qq * qq;

  *q = qq;

  *r = rr;

}

5、循環優化

1)、充分分解小的循環

要充分利用CPU的指令緩存,就要充分分解小的循環。特別是當循環體自己很小的時候,分解循環能夠提升性能。注意:不少編譯器並不能自動分解循環。 很差的代碼:

// 3D轉化:把矢量 V 和 4x4 矩陣 M 相乘

for (i = 0; i < 4; i ++)

{

  r[i] = 0;

  for (j = 0; j < 4; j ++)

  {

    r[i] +=   M[j][i]*V[j];

  }

}

推薦的代碼:

r[0] = M[0][0]*V[0]   + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];

r[1] = M[0][1]*V[0]   + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];

r[2] = M[0][2]*V[0]   + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];

r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];

2)、提取公共部分

對於一些不須要循環變量參加運算的任務能夠把它們放到循環外面,這裏的任務包括表達式、函數的調用、指針運算、數組訪問等,應該將沒有必要執行屢次的操做所有集合在一塊兒,放到一個init的初始化程序中進行。

3)、延時函數

一般使用的延時函數均採用自加的形式:

      void delay (void)

      {

unsigned int i;

      for (i=0;i<1000;i++) ;

      }

將其改成自減延時函數:

      void delay (void)

      {

unsigned int i;

          for (i=1000;i>0;i--) ;

      }

兩個函數的延時效果類似,但幾乎全部的C編譯對後一種函數生成的代碼均比前一種代碼少1~3個字節,由於幾乎全部的MCU均有爲0轉移的指令,採用後一種方式可以生成這類指令。在使用while循環時也同樣,使用自減指令控制循環會比使用自加指令控制循環生成的代碼更少1~3個字母。可是在循環中有經過循環變量「i」讀寫數組的指令時,使用預減循環有可能使數組超界,要引發注意。

4)、while循環和do…while循環

用while循環時有如下兩種循環形式:

unsigned int i;

      i=0;

      while (i<1000)

      {

          i++;

             //用戶程序

      }

或:

unsigned int i;

      i=1000;

do

{

            i--;

            //用戶程序

}

while (i>0);

在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短於while循環。

6)、循環展開

這是經典的速度優化,但許多編譯程序(如gcc -funroll-loops)能自動完成這個事,因此如今你本身來優化這個顯得效果不明顯。

舊代碼:

for (i = 0; i <   100; i++)

{

do_stuff(i);

}

新代碼:

for (i = 0; i <   100; )

{

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

}

能夠看出,新代碼裏比較指令由100次下降爲10次,循環時間節約了90%。不過注意:對於中間變量或結果被更改的循環,編譯程序每每拒絕展開,(怕擔責任唄),這時候就須要你本身來作展開工做了。

還有一點請注意,在有內部指令cache的CPU上(如MMX芯片),由於循環展開的代碼很大,每每cache溢出,這時展開的代碼會頻繁地在CPU 的cache和內存之間調來調去,又由於cache速度很高,因此此時循環展開反而會變慢。還有就是循環展開會影響矢量運算優化。

6)、循環嵌套

把相關循環放到一個循環裏,也會加快速度。

舊代碼:

for (i = 0; i <   MAX; i++)         /* initialize 2d array   to 0's */

      for (j = 0; j < MAX; j++)

          a[i][j] = 0.0;

      for (i = 0; i < MAX; i++)        /* put 1's   along the diagonal */

          a[i][i] = 1.0;

新代碼:

for (i = 0; i <   MAX; i++)         /* initialize 2d array   to 0's */

{

      for (j = 0; j < MAX; j++)

          a[i][j] = 0.0;

      a[i][i] = 1.0;                                  /* put 1's along the diagonal */

}

7)、Switch語句中根據發生頻率來進行case排序

Switch 可能轉化成多種不一樣算法的代碼。其中最多見的是跳轉表和比較鏈/樹。當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,匹配時就跳轉到知足條件的語句執行。因此能夠對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣能夠提升性能。此外,在case中推薦使用小的連續的整數,由於在這種狀況下,全部的編譯器均可以把switch 轉化成跳轉表。

很差的代碼:

int days_in_month, short_months, normal_months, long_months;

。。。。。。

switch   (days_in_month)

{

  case 28:

  case 29:

    short_months ++;

    break;

  case 30:

    normal_months ++;

    break;

  case 31:

    long_months ++;

    break;

  default:

    cout <<   "month has fewer than 28 or more than 31 days" << endl;

    break;

}

推薦的代碼:

int days_in_month, short_months, normal_months, long_months;

。。。。。。

switch   (days_in_month)

{

  case 31:

    long_months ++;

    break;

  case 30:

    normal_months ++;

    break;

  case 28:

  case 29:

    short_months ++;

    break;

  default:

    cout <<   "month has fewer than 28 or more than 31 days" << endl;

    break;

}  

8)、將大的switch語句轉爲嵌套switch語句

當switch語句中的case標號不少時,爲了減小比較的次數,明智的作法是把大switch語句轉爲嵌套switch語句。把發生頻率高的case 標號放在一個switch語句中,而且是嵌套switch語句的最外層,發生相對頻率相對低的case標號放在另外一個switch語句中。好比,下面的程序段把相對發生頻率低的狀況放在缺省的case標號內。

pMsg=ReceiveMessage();  

          switch (pMsg->type)

          {

          case FREQUENT_MSG1:

          handleFrequentMsg();

          break;

          case FREQUENT_MSG2:

          handleFrequentMsg2();

          break;

          。。。。。。

          case FREQUENT_MSGn:

          handleFrequentMsgn();

          break;

          default:                       //嵌套部分用來處理不常常發生的消息

          switch (pMsg->type)

          {

          case INFREQUENT_MSG1:

          handleInfrequentMsg1();

          break;

          case INFREQUENT_MSG2:

          handleInfrequentMsg2();

          break;

          。。。。。。

          case INFREQUENT_MSGm:

          handleInfrequentMsgm();

          break;

          }

          }

若是switch中每一種狀況下都有不少的工做要作,那麼把整個switch語句用一個指向函數指針的表來替換會更加有效,好比下面的switch語句,有三種狀況:

      enum MsgType{Msg1, Msg2, Msg3}

          switch (ReceiveMessage()

          {

          case Msg1;

          。。。。。。

          case Msg2;

          。。。。。

          case Msg3;

          。。。。。

          }

爲了提升執行速度,用下面這段代碼來替換這個上面的switch語句。

          /*準備工做*/

          int handleMsg1(void);

          int handleMsg2(void);

          int handleMsg3(void);

          /*建立一個函數指針數組*/

          int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3};

          /*用下面這行更有效的代碼來替換switch語句*/

          status=MsgFunction[ReceiveMessage()]();

9)、循環轉置

有些機器對JNZ(爲0轉移)有特別的指令處理,速度很是快,若是你的循環對方向不敏感,能夠由大向小循環。

舊代碼:

for (i = 1; i <=   MAX; i++)

{

   。。。

 }

新代碼:

i = MAX+1;

while (--i)

{

。。。

}

不過千萬注意,若是指針操做使用了i值,這種方法可能引發指針越界的嚴重錯誤(i = MAX+1;)。固然你能夠經過對i作加減運算來糾正,可是這樣就起不到加速的做用,除非相似於如下狀況:

舊代碼:

char a[MAX+5];

for (i = 1; i <=   MAX; i++)

{

*(a+i+4)=0;

}

新代碼:

i = MAX+1;

while (--i)

{

        *(a+i+4)=0;

}

10)、公用代碼塊

一些公用處理模塊,爲了知足各類不一樣的調用須要,每每在內部採用了大量的if-then-else結構,這樣很很差,判斷語句若是太複雜,會消耗大量的時間的,應該儘可能減小公用代碼塊的使用。(任何狀況下,空間優化和時間優化都是對立的--東樓)。固然,若是僅僅是一個(3==x)之類的簡單判斷,適當使用一下,也仍是容許的。記住,優化永遠是追求一種平衡,而不是走極端。

11)提高循環的性能

要提高循環的性能,減小多餘的常量計算很是有用(好比,不隨循環變化的計算)。

很差的代碼(在for()中包含不變的if()):

for( i 。。。 )

{

  if( CONSTANT0 )

  {

    DoWork0( i ); // 假設這裏不改變CONSTANT0的值

  }

  else

  {

    DoWork1( i ); // 假設這裏不改變CONSTANT0的值

  }

}

推薦的代碼:

if( CONSTANT0 )

{

  for( i 。。。 )

  {

    DoWork0( i );

  }

}

else

{

  for( i 。。。 )

  {

    DoWork1( i );

  }

若是已經知道if()的值,這樣能夠避免重複計算。雖然很差的代碼中的分支能夠簡單地預測,可是因爲推薦的代碼在進入循環前分支已經肯定,就能夠減小對分支預測的依賴。

12)、選擇好的無限循環

在編程中,咱們經常須要用到無限循環,經常使用的兩種方法是while (1) 和 for (;;)。這兩種方法效果徹底同樣,但那一種更好呢?然咱們看看它們編譯後的代碼:

編譯前:

while (1);

編譯後:

mov eax,1

test eax,eax

je foo+23h

jmp foo+18h 

編譯前:

for (;;);

編譯後:

jmp foo+23h

顯然,for (;;)指令少,不佔用寄存器,並且沒有判斷、跳轉,比while (1)好。

6、提升CPU的並行性

1)使用並行代碼

儘量把長的有依賴的代碼鏈分解成幾個能夠在流水線執行單元中並行執行的沒有依賴的代碼鏈。不少高級語言,包括C++,並不對產生的浮點表達式從新排序,由於那是一個至關複雜的過程。須要注意的是,重排序的代碼和原來的代碼在代碼上一致並不等價於計算結果一致,由於浮點操做缺少精確度。在一些狀況下,這些優化可能致使意料以外的結果。幸運的是,在大部分狀況下,最後結果可能只有最不重要的位(即最低位)是錯誤的。

很差的代碼:

double a[100], sum;

int i;

sum = 0.0f;

for (i=0; i<100; i++)

sum += a[i];

推薦的代碼:

double a[100], sum1, sum2, sum3, sum4, sum;

int i;

sum1 = sum2 = sum3   = sum4 = 0.0;

for (i = 0; i < 100; i += 4)

{

  sum1 += a[i];

  sum2 += a[i+1];

  sum3 += a[i+2];

  sum4 += a[i+3];

}

sum =   (sum4+sum3)+(sum1+sum2); 

要注意的是:使用4路分解是由於這樣使用了4段流水線浮點加法,浮點加法的每個段佔用一個時鐘週期,保證了最大的資源利用率。

2)避免沒有必要的讀寫依賴

當數據保存到內存時存在讀寫依賴,即數據必須在正確寫入後才能再次讀取。雖然AMD Athlon等CPU有加速讀寫依賴延遲的硬件,容許在要保存的數據被寫入內存前讀取出來,可是,若是避免了讀寫依賴並把數據保存在內部寄存器中,速度會更快。在一段很長的又互相依賴的代碼鏈中,避免讀寫依賴顯得尤爲重要。若是讀寫依賴發生在操做數組時,許多編譯器不能自動優化代碼以免讀寫依賴。因此推薦程序員手動去消除讀寫依賴,舉例來講,引進一個能夠保存在寄存器中的臨時變量。這樣能夠有很大的性能提高。下面一段代碼是一個例子:

很差的代碼:

float x[VECLEN], y[VECLEN], z[VECLEN];

。。。。。。

for (unsigned int k   = 1; k < VECLEN; k ++)

{

  x[k] = x[k-1] +   y[k];

}

for (k = 1; k <VECLEN; k++)

{

  x[k] = z[k] * (y[k]   - x[k-1]);

}

推薦的代碼:

float x[VECLEN], y[VECLEN], z[VECLEN];

。。。。。。

float t(x[0]);

for (unsigned int k   = 1; k < VECLEN; k ++)

{

  t = t + y[k];

  x[k] = t;

}

t = x[0];

for (k = 1; k <; VECLEN; k ++)

{

  t = z[k] * (y[k] -   t);

  x[k] = t;

7、循環不變計算

對於一些不須要循環變量參加運算的計算任務能夠把它們放到循環外面,如今許多編譯器仍是能本身幹這件事,不過對於中間使用了變量的算式它們就不敢動了,因此不少狀況下你還得本身幹。對於那些在循環中調用的函數,凡是不必執行屢次的操做統統提出來,放到一個init函數裏,循環前調用。另外儘可能減小餵食次數,不必的話儘可能不給它傳參,須要循環變量的話讓它本身創建一個靜態循環變量本身累加,速度會快一點。

還有就是結構體訪問,東樓的經驗,凡是在循環裏對一個結構體的兩個以上的元素執行了訪問,就有必要創建中間變量了(結構這樣,那C++的對象呢?想一想看),看下面的例子:

舊代碼:

      total =

      a->b->c[4]->aardvark +

      a->b->c[4]->baboon +

      a->b->c[4]->cheetah +

      a->b->c[4]->dog;

新代碼:

      struct animals * temp = a->b->c[4];

      total =

      temp->aardvark +

      temp->baboon +

      temp->cheetah +

      temp->dog;

一些老的C語言編譯器不作聚合優化,而符合ANSI規範的新的編譯器能夠自動完成這個優化,看例子:

      float a, b, c, d, f, g;

    。。。

      a = b / c * d;

      f = b * g / c;

這種寫法固然要得,可是沒有優化

      float a, b, c, d, f, g;

    。。。

      a = b / c * d;

      f = b / c * g;

若是這麼寫的話,一個符合ANSI規範的新的編譯器能夠只計算b/c一次,而後將結果代入第二個式子,節約了一次除法運算。

8、函數優化

1Inline函數

在C++中,關鍵字Inline能夠被加入到任何函數的聲明中。這個關鍵字請求編譯器用函數內部的代碼替換全部對於指出的函數的調用。這樣作在兩個方面快於函數調用:第一,省去了調用指令須要的執行時間;第二,省去了傳遞變元和傳遞過程須要的時間。可是使用這種方法在優化程序速度的同時,程序長度變大了,所以須要更多的ROM。使用這種優化在Inline函數頻繁調用而且只包含幾行代碼的時候是最有效的。

2)不定義不使用的返回值

函數定義並不知道函數返回值是否被使用,假如返回值歷來不會被用到,應該使用void來明確聲明函數不返回任何值。

3)減小函數調用參數

    使用全局變量比函數傳遞參數更加有效率。這樣作去除了函數調用參數入棧和函數完成後參數出棧所須要的時間。然而決定使用全局變量會影響程序的模塊化和重入,故要慎重使用。

4)全部函數都應該有原型定義

通常來講,全部函數都應該有原型定義。原型定義能夠傳達給編譯器更多的可能用於優化的信息。

5)儘量使用常量(const)

儘量使用常量(const)。C++ 標準規定,若是一個const聲明的對象的地址不被獲取,容許編譯器不對它分配儲存空間。這樣可使代碼更有效率,並且能夠生成更好的代碼。

6)把本地函數聲明爲靜態的(static)

若是一個函數只在實現它的文件中被使用,把它聲明爲靜態的(static)以強制使用內部鏈接。不然,默認的狀況下會把函數定義爲外部鏈接。這樣可能會影響某些編譯器的優化——好比,自動內聯。

9、採用遞歸

與LISP之類的語言不一樣,C語言一開始就病態地喜歡用重複代碼循環,許多C程序員都是除非算法要求,堅定不用遞歸。事實上,C編譯器們對優化遞歸調用一點都不反感,相反,它們還很喜歡幹這件事。只有在遞歸函數須要傳遞大量參數,可能形成瓶頸的時候,才應該使用循環代碼,其餘時候,仍是用遞歸好些。

10、變量

1register變量

在聲明局部變量的時候可使用register關鍵字。這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,合理使用這種方法能夠提升執行速度。函數調用越是頻繁,越是可能提升代碼的速度。

在最內層循環避免使用全局變量和靜態變量,除非你能肯定它在循環週期中不會動態變化,大多數編譯器優化變量都只有一個辦法,就是將他們置成寄存器變量,而對於動態變量,它們乾脆放棄對整個表達式的優化。儘可能避免把一個變量地址傳遞給另外一個函數,雖然這個還很經常使用。C語言的編譯器們老是先假定每個函數的變量都是內部變量,這是由它的機制決定的,在這種狀況下,它們的優化完成得最好。可是,一旦一個變量有可能被別的函數改變,這幫兄弟就不再敢把變量放到寄存器裏了,嚴重影響速度。看例子:

a = b();

c(&d);

由於d的地址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裏,一旦運行到c(&d),編譯器就把它放回內存,若是在循環裏,會形成N次頻繁的在內存和寄存器之間讀寫d的動做,衆所周知,CPU在系統總線上的讀寫速度慢得很。好比你的賽楊300,CPU主頻300,總線速度最多66M,爲了一個總線讀,CPU可能要等4-5個週期,得。。得。。得。。想起來都打顫。

2)、同時聲明多個變量優於單獨聲明變量

3)、短變量名優於長變量名,應儘可能使變量名短一點

4)、在循環開始前聲明變量

11、使用嵌套的if結構

在if結構中若是要判斷的並列條件較多,最好將它們拆分紅多個if結構,而後嵌套在一塊兒,這樣能夠避免無謂的判斷。

說明:

上面的優化方案由王全明收集整理。不少資料來源與網上,出處不祥,在此對全部做者一併致謝!

該方案主要是考慮到在嵌入式開發中對程序執行速度的要求特別高,因此該方案主要是爲了優化程序的執行速度。

注意:優化是有側重點的,優化是一門平衡的藝術,它每每要以犧牲程序的可讀性或者增長代碼長度爲代價。

(任何狀況下,空間優化和時間優化都是對立的--東樓)。 

相關文章
相關標籤/搜索