1. 程式人生 > 其它 >【轉】嵌入式C語言程式碼優化方案

【轉】嵌入式C語言程式碼優化方案

來源:嵌入式C語言程式碼優化方案(深度好文,建議花時間研讀並收藏) (qq.com)

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迴圈。

(5)迴圈展開

這是經典的速度優化,但許多編譯程式(如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、函式優化

(1)Inline函式

在C++中,關鍵字Inline可以被加入到任何函式的宣告中。這個關鍵字請求編譯器用函式內部的程式碼替換所有對於指出的函式的呼叫。這樣做在兩個方面快於函式呼叫:第一,省去了呼叫指令需要的執行時間;第二,省去了傳遞變元和傳遞過程需要的時間。但是使用這種方法在優化程式速度的同時,程式長度變大了,因此需要更多的ROM。使用這種優化在Inline函式頻繁呼叫並且只包含幾行程式碼的時候是最有效的。

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

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

(3)減少函式呼叫引數

使用全域性變數比函式傳遞引數更加有效率。這樣做去除了函式呼叫引數入棧和函式完成後引數出棧所需要的時間。然而決定使用全域性變數會影響程式的模組化和重入,故要慎重使用。

(4)所有函式都應該有原型定義

一般來說,所有函式都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於優化的資訊。

(5)儘可能使用常量(const)

儘可能使用常量(const)。C++ 標準規定,如果一個const宣告的物件的地址不被獲取,允許編譯器不對它分配儲存空間。這樣可以使程式碼更有效率,而且可以生成更好的程式碼。

(6)把本地函式宣告為靜態的(static)

如果一個函式只在實現它的檔案中被使用,把它宣告為靜態的(static)以強制使用內部連線。否則,預設的情況下會把函式定義為外部連線。這樣可能會影響某些編譯器的優化——比如,自動內聯。

9、採用遞迴

與LISP之類的語言不同,C語言一開始就病態地喜歡用重複程式碼迴圈,許多C程式設計師都是除非演算法要求,堅決不用遞迴。事實上,C編譯器們對優化遞迴呼叫一點都不反感,相反,它們還很喜歡幹這件事。只有在遞迴函式需要傳遞大量引數,可能造成瓶頸的時候,才應該使用迴圈程式碼,其他時候,還是用遞迴好些。

10、變數

(1)register變數

在宣告區域性變數的時候可以使用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結構,然後巢狀在一起,這樣可以避免無謂的判斷。

說明:

上面的優化方案由王全明收集整理。很多資料來源於網上,出處不祥,在此對所有作者一併致謝!

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

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

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