代碼 優化 指南 實踐

C代碼優化方案

?

?

?

?

?

?

?

?

?

?

華中科技大學計算機學院

?

姓名:??? 王全明

QQ?????? 375288012

Email????quanming1119@163.com


目錄

目錄

C代碼優化方案

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

2、使用盡量小的數據類型

3、減少運算的強度

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

(2)、求余運算

(3)、平方運算

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

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

(6)、使用增量和減量操作符

(7)、使用復合賦值表達式

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

4、結構體成員的布局

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

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

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

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

5、循環優化

(1)、充分分解小的循環

(2)、提取公共部分

(3)、延時函數

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

(6)、循環展開

(6)、循環嵌套

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

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

(9)、循環轉置

(10)、公用代碼塊

(11)提升循環的性能

(12)、選擇好的無限循環

6、提高CPU的并行性

(1)使用并行代碼

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

7、循環不變計算

8、函數

(1)Inline函數

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

(3)減少函數調用參數

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

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

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

9、采用遞歸

10、變量

(1)register變量

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

(3)、短變量名優于長變量名,應盡量使變量名短一點

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

11、使用嵌套的if結構


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(a30);

更改為:

??? 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 Ax????? ;x從內存取出存入累加器A

??? add A1??????? ;累加器A1

store x????????? ;把新值存回x

如果使用增量操作符,生成的代碼如下:

??? incr x?????????? ;x1

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

(7)、使用復合賦值表達式

復合賦值表達式(a-=1a+=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);

在這兩種循環中,使用dowhile循環編譯后生成的代碼的長度短于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%不過注意:對于中間變量或結果被更改的循環,編譯程序往往拒絕展開,(怕擔責任唄),這時候就需要你自己來做展開工作了。

還有一點請注意,在有內部指令cacheCPU(MMX芯片),因為循環展開的代碼很大,往往cache溢出,這時展開的代碼會頻繁地在CPUcache和內存之間調來調去,又因為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 eax1

test eaxeax

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 AthlonCPU有加速讀寫依賴延遲的硬件,允許在要保存的數據被寫入內存前讀取出來,但是,如果避免了讀寫依賴并把數據保存在內部寄存器中,速度會更快。在一段很長的又互相依賴的代碼鏈中,避免讀寫依賴顯得尤其重要。如果讀寫依賴發生在操作數組時,許多編譯器不能自動優化代碼以避免讀寫依賴。所以推薦程序員手動去消除讀寫依賴,舉例來說,引進一個可以保存在寄存器中的臨時變量。這樣可以有很大的性能提升。下面一段代碼是一個例子:

不好的代碼:

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結構,然后嵌套在一起,這樣可以避免無謂的判斷。


說明:

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

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

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

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


2.?

代碼優化概要

我編寫程序至今有35年了,我做了很多關于程序執行速度方面優化的工( 一個示例),我也看過其它人做的優化。我發現有兩個最基本的優化技術總是被人所忽略。 注意,這兩個技術并不是避免時機不成熟的優化。并不是把冒泡排序變成快速排序(算法優化)。也不是語言或是編譯器的優化。也不是把 i*4寫成i<<2 的優化。 這兩個技術是:
  1. 使用 一個profiler。
  2. 查看程序執行時的匯編碼。

使用這兩個技術的人將會成功地寫出運行快的代碼,不會使用這兩個技術的人則不行。下面讓我為你細細道來。

使用一個 Profiler

我們知道,程序運行時的90%的時間是用在了10%的代碼上。我發現這并不準確。一次又一次地,我發現,幾乎所有的程序會在1%的代碼上花了99%的運行時間。但是,是哪個1%?一個好的Profiler可以告訴你這個答案。就算我們需要使用100個小時在這1%的代碼上進行優化,也比使用100個小時在其它99%的代碼上優化產生的效益要高得多得多。 問題是什么?人們不用profiler?不是。我工作過的一個地方使用了一個華麗而奢侈的Profiler,但是自從購買這個Profiler后,它的包裝3年來還是那么的暫新。為什么人們不用?我真的不知道。有一次,我和我的同事去了一個負載過大的交易所,我同事堅持說他知道哪里是瓶頸,畢竟,他是一個很有經驗的專家。最終,我把我的Profiler在他的項目上運行了一下,我們發現那個瓶頸完全在一個意想不到的地方。 就像是賽車一樣。團隊是贏在傳感器和日志上,這些東西提供了所有的一切。你可以調整一下賽車手的褲子以讓其在比賽過程中更舒服,但是這不會讓你贏得比賽,也不會讓你更有競爭力。如果你不知道你的速度上不去是因為引擎、排氣裝置、空體動力學、輪胎氣壓,或是賽車手,那么你將無法獲勝。編程為什么會不同呢?只要沒有測量,你就永遠無法進步。 這個世界上有太多可以使用的Profiler了。隨便找一個你就可以看到你的函數的調用層次,調用的次數,以前每條代碼的時間分解表(甚至可以到匯編級)。我看過太多的程序員回避使用Profiler,而是把時間花在那些無用的,錯誤的方向上的“優化”,而被其競爭對手所羞辱。(譯者陳皓注:使用Profiler時,重點需要關注:1)花時間多的函數以優化其算法,2)調用次數巨多的函數——如果一個函數每秒被調用300K次,你只需要優化出0.001毫秒,那也是相當大的優化。這就是作者所謂的1%的代碼占用了99%的CPU時間)

查看匯編代碼

幾年前,我有一個同事,Mary Bailey,她在華盛頓大學教矯正代數(remedial algebra),有一次,她在黑板上寫下: x + 3 = 5 然后問他的學生“求解x”,然后學生們不知道答案。于是她寫下:__ + 3 = 5 然后,再問學生“填空”,所有的學生都可以回答了。未知數x就像是一個有魔法的字母讓大家都在想“x意味著代數,而我沒有學過代數,所以我就不知道這個怎么做”。 匯編程序就是編程世界的代數。如果某人問我“inline函數是否被編譯器展開了?”或是問我“如果我寫下i*4,編譯器會把其優化為左移位操作嗎?”。這個時候,我都會建議他們看看編譯器的匯編碼。這樣的回答是不是很粗暴和無用?通常,在我這樣回答了提問者后,提問都通常都會說,對不起,我不知道什么是匯編!甚至C++的專家都會這么回答。 匯編語言是最簡單的編程語言了(就算是和C++相比也是這樣的),如:

ADD ESI,x

就是(C風格的代碼)

ESI += x;

而:

CALL foo

則是:

foo();

細節因為CPU的種類而不同,但這就是其如何工作的。有時候,我們甚至都不需要細節,只需要看看匯編碼的長啥樣,然后和源代碼比一比,你就可以知道匯編代碼很多很多了。 那么,這又如何幫助代碼優化?舉個例子,我幾年前認識一個程序員認為他應該去發現一個新的更快的算法。他有一個benchmark來證明這個算法,并且其寫了一篇非常漂亮的文章關于他的這個算法。但是,有人看了一下其原來算法以及新算法的匯編,發現了他的改進版本的算法允許其編譯器把兩個除法操作變成了一個。這和算法真的沒有什么關系。我們知道除法操作是一個很昂貴的操作,并且在其算法中,這倆個除法操作還在一個內嵌循環中,所以,他的改進版的算法當然要快一些。但,只需要在原來的算法上做一點點小的改動——使用一個除法操作,那么其原來的算法將會和新的一樣快。而他的新發現什么也不是。 下一個例子,一個D用戶張貼了一個 benchmark 來顯示 dmd (Digital Mars D 編譯器)在整型算法上的很糟糕,而ldc (LLVM D 編譯器) 就好很多了。對于這樣的結果,其相當的有意見。我迅速地看了一下匯編,發現兩個編譯器編譯出來相當的一致,并沒有什么明顯的東西要對2:1這么大的不同而負責。但是我們看到有一個對long型整數的除法,這個除法調用了運行庫。而這個庫成為消耗時間的殺手,其它所有的加減法都沒有速度上的影響。出乎意料地,benchmark 和算法代碼生成一點關系也沒有,完全就是long型整數的除法的問題。這暴露了在dmd的運行庫中的long型除法的實現很差。修正后就可以提高速度。所以,這和編譯器沒有什么關系,但是如果不看匯編,你將無法發現這一切。 查看匯編代碼經常會給你一些意想不到的東西讓你知道為什么程序的性能是那樣。一些意想不到的函數調用,預料不到的自傲,以及不應該存在的東西,等等其實所有的一切。但也不需要成為一個匯編代碼的黑客才能干的事。

結論

如果你覺得需要程序有更好的執行速度,那么,最基本的方法就是使用一個profiler和愿意去查看一下其匯編代碼以找到程序的瓶頸。只有找到了程序的瓶頸,此時才是真正在思考如何去改進的時候,比如思考一個更好的算法,使用更快的語言優化,等等。 常規的做法是制勝法寶是挑選一個最佳的算法而不是進行微優化。雖然這種做法是無可異議的,但是有兩件事情是學校沒有教給你而需要你重點注意的。第一個也是最重要的,如果你優化的算法沒沒有參與到你程序性能中的算法,那么你優化他只是在浪費時間和精力,并且還轉移了你的注意力讓你錯過了應該要去優化的部分。第二點,算法的性能總和處理的數據密切相關的,就算是冒泡排序有那么多的笑柄,但是如果其處理的數據基本是排好序的,只有其中幾個數據是未排序的,那么冒泡排序也是所有排序算法里性能最好的。所以,擔心沒有使用好的算法而不去測量,只會浪費時間,無論是你的還是計算機的。 就好像賽車零件的訂購速底是不會讓你更靠進冠軍(就算是你正確安裝零件也不會),沒有Profiler,你不會知道問題在哪里,不去看匯編,你可能知道問題所在,但你往往不知道為什么。 (全文完)


3.?

優化代碼

通過優化可執行文件,可在較快執行速度和較小代碼大小之間實現平衡。 本主題討論了 Visual C++ 提供的可幫助您優化代碼的一些機制。

語言功能

下面的主題介紹了 C/C++ 語言中的一些優化功能。

優化雜注和關鍵字

可在代碼中使用以提高性能的關鍵字和雜注的列表。

按類別列出的編譯器選項

專門影響執行速度或代碼大小的 /O 編譯器選項的列表。

Rvalue Reference Declarator: &&

Rvalue 引用支持移動語義的實現。 如果移動語義用于實現模板庫,則使用這些模板的應用程序的性能可顯著提高。

優化雜注

如果經過優化的某個代碼節導致錯誤或速度減慢,則可以使用 optimize 雜注對該代碼節關閉優化。

用兩個雜注將代碼括起來,如下所示:

#pragma optimize("", off)
// some code here 
#pragma optimize("", on)
編程慣例

在用優化的方式編譯代碼時,您可能會注意到一些附加的警告消息。 此行為是預期行為,因為一些警告僅與優化的代碼有關。 如果您注意到這些警告,則可以避免許多優化問題。

矛盾的是,為了速度而對程序進行優化可能會導致代碼運行速度減慢。 這是因為一些為了速度而進行的優化會增加代碼大小。 例如,內聯函數可消除函數調用的開銷。但是內聯太多代碼可能會使程序很大,致使虛擬內存頁的錯誤數增加。 因此,通過消除函數調用獲得的速度可能會丟失在內存交調中。

下面的主題討論了良好的編程做法。

提高時間關鍵代碼的技巧

更好的編碼技術可產生更好的性能。 本主題建議了一些可幫助您確保時間關鍵代碼部分的執行令人滿意的編碼技術。

優化最佳做法

提供了有關如何以最佳方式優化應用程序的一般準則。

調試優化的代碼

由于優化可能會更改編譯器創建的代碼,因此建議您調試應用程序并測量其性能,隨后優化代碼。

下面的主題提供有關如何進行調試的基本信息。

  • 使用 Visual Studio 進行調試

  • 創建發行版本時遇到的常見問題

下面的主題提供有關如何進行調試的更高級信息。

  • 如何:調試優化的代碼

  • 為何浮點數可能丟失精度

以下各個主題提供有關如何優化生成、加載和執行代碼的信息。

  • 提高編譯器吞吐量

  • 使用沒有 () 的函數名不產生代碼

  • Optimizing Inline Assembly

  • 為 ATL 項目指定編譯器優化

  • 加載時應使用哪些優化技術來提高客戶端應用程序的性能?

  • 有關以下內容的更多信息如何縮短 DLL 方法加載時間的更多信息,請參見 MSDN 庫網站上的“MSDN 雜志”中“Under the Hood”(深入實質)專欄下的“Optimizing DLL Load Time Performance”(優化 DLL 加載時間性能)。

  • 有關以下內容的更多信息如何在應用程序中最大程度減少分頁的更多信息,請參見 MSDN 庫 網站上的“MSDN 雜志”中“Bugslayer”專欄下的“Improving Runtime Performance with the Smooth Working Set Tool”(使用 Smooth 工作集工具提高運行時性能)和“Improving Runtime Performance with the Smooth Working Set Tool—Part 2”(使用 Smooth 工作集工具提高運行時性能(第 2 部分))。


4.?

C++代碼優化方法總結

優化是一個非常大的主題,本文并不是去深入探討性能分析理論,算法的效率,況且我也沒有這個能力。我只是想把一些可以簡單的應用到你的C++代碼中的優化技術總結在這里,這樣,當你遇到幾種不同的編程策略的時候,就可以對每種策略的性能進行一個大概的估計。這也是本文的目的之所在。

一. 優化之前
在進行優化之前,我們首先應該做的是發現我們代碼的瓶頸(bottleneck)在哪里。然而當你做這件事情的時候切忌從一個debug-version進行推斷,因為debug-version中包含了許多額外的代碼。一個debug-version可執行體要比release-version大出40%。那些額外的代碼都是用來支持調試的,比如說符號的查找。大多數實現都為debug-version和release-version提供了不同的operator new以及庫函數。而且,一個release-version的執行體可能已經通過多種途徑進行了優化,包括不必要的臨時對象的消除,循環展開,把對象移入寄存器,內聯等等。
另外,我們要把調試和優化區分開來,它們是在完成不同的任務。 debug-version 是用來追捕bugs以及檢查程序是否有邏輯上的問題。release-version則是用來做一些性能上的調整以及進行優化。
下面就讓我們來看看有哪些代碼優化技術吧:

二. 聲明的放置
程序中變量和對象的聲明放在什么位置將會對性能產生顯著影響。同樣,對postfix和prefix運算符的選擇也會影響性能。這一部分我們集中討論四個問題:初始化v.s 賦值,在程序確實要使用的地方放置聲明,構造函數的初始化列表,prefix v.s postfix運算符。
(1) 請使用初始化而不是賦值
在C語言中只允許在一個函數體的開頭進行變量的聲明,然而在C++中聲明可以出現在程序的任何位置。這樣做的目的是希望把對象的聲明拖延到確實要使用它的時候再進行。這樣做可以有兩個好處:1. 確保了對象在它被使用前不會被程序的其他部分惡意修改。如果對象在開頭就被聲明然而卻在20行以后才被使用的話,就不能做這樣的保證。2. 使我們有機會通過用初始化取代賦值來達到性能的提升,從前聲明只能放在開頭,然而往往開始的時候我們還沒有獲得我們想要的值,因此初始化所帶來的好處就無法被應用。但是現在我們可以在我們獲得了想要的值的時候直接進行初始化,從而省去了一步。注意,或許對于基本類型來說,初始化和賦值之間可能不會有什么差異,但是對于用戶定義的類型來說,二者就會帶來顯著的不同,因為賦值會多進行一次函數調用----operator =。因此當我們在賦值和初始化之間進行選擇的話,初始化應該是我們的首選。
(2) 把聲明放在合適的位置上
在一些場合,通過移動聲明到合適的位置所帶來的性能提升應該引起我們足夠的重視。例如:
bool is_C_Needed();
void use()
{
C c1;
if (is_C_Needed() == false)
{
return; //c1 was not needed
}
//use c1 here
return;
}
上面這段代碼中對象c1即使在有可能不使用它的情況下也會被創建,這樣我們就會為它付出不必要的花費,有可能你會說一個對象c1能浪費多少時間,但是如果是這種情況呢:C c1[1000];我想就不是說浪費就浪費了。但是我們可以通過移動聲明c1的位置來改變這種情況:
void use()
{
if (is_C_Needed() == false)
{
return; //c1 was not needed
}
C c1; //moved from the block"s beginning
//use c1 here
return;
}
怎么樣,程序的性能是不是已經得到很大的改善了呢?因此請仔細分析你的代碼,把聲明放在合適的位置上,它所帶來的好處是你難以想象的。
(3) 初始化列表
我們都知道,初始化列表一般是用來初始化const或者reference數據成員。但是由于他自身的性質,我們可以通過使用初始化列表來實現性能的提升。我們先來看一段程序:
class Person
{
private:
C c_1;
C c_2;
public:
Person(const C & c1, const C& c2 ): c_1(c1), c_2(c2) {}
};
當然構造函數我們也可以這樣寫:
Person::Person(const C& c1, const C& c2)
{
c_1 = c1;
c_2 = c2;
}
那么究竟二者會帶來什么樣的性能差異呢,要想搞清楚這個問題,我們首先要搞清楚二者是如何執行的,先來看初始化列表:數據成員的聲明操作都是在構造函數執行之前就完成了,在構造函數中往往完成的只是賦值操作,然而初始化列表直接是在數據成員聲明的時候就進行了初始化,因此它只執行了一次copy constructor。再來看在構造函數中賦值的情況:首先,在構造函數執行前會通過default constructor創建數據成員,然后在構造函數中通過operator =進行賦值。因此它就比初始化列表多進行了一次函數調用。性能差異就出來了。但是請注意,如果你的數據成員都是基本類型的話,那么為了程序的可讀性就不要使用初始化列表了,因為編譯器對兩者產生的匯編代碼是相同的。
(4) postfix VS prefix 運算符
prefix運算符++和—比它的postfix版本效率更高,因為當postfix運算符被使用的時候,會需要一個臨時對象來保存改變以前的值。對于基本類型,編譯器會消除這一份額外的拷貝,但是對于用戶定義類型,這似乎是不可能的。因此請你盡可能使用prefix運算符。

三. 內聯函數
內聯函數既能夠去除函數調用所帶來的效率負擔又能夠保留一般函數的優點。然而,內聯函數并不是萬能藥,在一些情況下,它甚至能夠降低程序的性能。因此在使用的時候應該慎重。
1.我們先來看看內聯函數給我們帶來的好處:從一個用戶的角度來看,內聯函數看起來和普通函數一樣,它可以有參數和返回值,也可以有自己的作用域,然而它卻不會引入一般函數調用所帶來的負擔。另外,它可以比宏更安全更容易調試。
當然有一點應該意識到,inline specifier僅僅是對編譯器的建議,編譯器有權利忽略這個建議。那么編譯器是如何決定函數內聯與否呢?一般情況下關鍵性因素包括函數體的大小,是否有局部對象被聲明,函數的復雜性等等。
2.那么如果一個函數被聲明為inline但是卻沒有被內聯將會發生什么呢?理論上,當編譯器拒絕內聯一個函數的時候,那個函數會像普通函數一樣被對待,但是還會出現一些其他的問題。例如下面這段代碼:
// filename Time.h
#include<ctime>
#include<iostream>
using namespace std;
class Time
{
public:
inline void Show() { for (int i = 0; i <10; i++) cout<<time(0)<<endl;}
};
因為成員函數Time::Show()包括一個局部變量和一個for循環,所以編譯器一般拒絕inline,并且把它當作一個普通的成員函數。但是這個包含類聲明的頭文件會被單獨的#include進各個獨立的編譯單元中:
// filename f1.cpp
#include "Time.hj"
void f1()
{
Time t1;
t1.Show();
}

// filename f2.cpp
#include "Time.h"
void f2()
{
Time t2;
t2.Show();
}
結果編譯器為這個程序生成了兩個相同成員函數的拷貝:
void f1();
void f2();
int main()
{
f1();
f2();
return 0;
}
當程序被鏈接的時候,linker將會面對兩個相同的Time::Show()拷貝,于是函數重定義的連接錯誤發生。但是老一些的C++實現對付這種情況的辦法是通過把一個un-inlined函數當作static來處理。因此每一份函數拷貝僅僅在自己的編譯單元中可見,這樣鏈接錯誤就解決了,但是在程序中卻會留下多份函數拷貝。在這種情況下,程序的性能不但沒有提升,反而增加了編譯和鏈接時間以及最終可執行體的大小。
但是幸運的是,新的C++標準中關于un-inlined函數的說法已經改變。一個符合標準C++實現應該只生成一份函數拷貝。然而,要想所有的編譯器都支持這一點可能還需要很長時間。
另外關于內聯函數還有兩個更令人頭疼的問題。第一個問題是該如何進行維護。一個函數開始的時候可能以內聯的形式出現,但是隨著系統的擴展,函數體可能要求添加額外的功能,結果內聯函數就變得不太可能,因此需要把inline specifier去除以及把函數體放到一個單獨的源文件中。另一個問題是當內聯函數被應用在代碼庫的時候產生。當內聯函數改變的時候,用戶必須重新編譯他們的代碼以反映這種改變。然而對于一個非內聯函數,用戶僅僅需要重新鏈接就可以了。
這里想要說的是,內聯函數并不是一個增強性能的靈丹妙藥。只有當函數非常短小的時候它才能得到我們想要的效果,但是如果函數并不是很短而且在很多地方都被調用的話,那么將會使得可執行體的體積增大。最令人煩惱的還是當編譯器拒絕內聯的時候。在老的實現中,結果很不盡人意,雖然在新的實現中有很大的改善,但是仍然還是不那么完善的。一些編譯器能夠足夠的聰明來指出哪些函數可以內聯哪些不能,但是,大多數編譯器就不那么聰明了,因此這就需要我們的經驗來判斷。如果內聯函數不能增強行能,就避免使用它!

四. 優化你的內存使用
通常優化都有幾個方面:更快的運行速度,有效的系統資源使用,更小的內存使用。一般情況下,代碼優化都是試圖在以上各個方面進行改善。重新放置聲明技術被證明是消除多余對象的建立和銷毀,這樣既減小了程序的大小又加快了運行速度。然而其他的優化技術都是基于一個方面------更快的速度或者是更小的內存使用。有時,這些目標是互斥的,壓縮了內存的使用往往卻減慢了代碼速度,快速的代碼卻又需要更多的內存支持。下面總結兩種在內存使用上的優化方法:
1. Bit Fields
在C/C++中都可以存取和訪問數據的最小組成單元:bit。因為bit并不是C/C++基本的存取單元,所以這里是通過犧牲運行速度來減少內存和輔助存儲器的空間的使用。注意:一些硬件結構可能提供了特殊的處理器指令來存取bit,因此bit fields是否影響程序的速度取決于具體平臺。
在我們的現實生活中,一個數據的許多位都被浪費了,因為某些應用根本就不會有那么大的數據范圍。也許你會說,bit是如此之小,通過它就能減小存儲空間的使用嗎?的確,在數據量很小的情況下不會看出什么效果,但是在數據量驚人的情況下,它所節省的空間還是能夠讓我們的眼睛為之一亮的。也許你又會說,現在內存和硬盤越來越便宜,何苦要費半天勁,這省不了幾個錢。但是還有另外一個原因一定會使你信服,那就是數字信息傳輸。一個分布式數據庫都會在不同的地點有多份拷貝。那么數百萬的紀錄傳輸就會顯得十分昂貴。Ok,現在我們就來看看該如何做吧,首先看下面這段代碼:
struct BillingRec
{
long cust_id;
long timestamp;
enum CallType
{
toll_free,
local,
regional,
long_distance,
international,
cellular
} type;
enum CallTariff
{
off_peak,
medium_rate,
peak_time
} tariff;
};
上面這個結構體在32位的機器上將會占用16字節,你會發現其中有許多位都被浪費了,尤其是那兩個enum型,浪費更是嚴重,所以請看下面做出的改進:
struct BillingRec
{
int cust_id: 24; // 23 bits + 1 sign bit
int timestamp: 24;
enum CallType
{//...
};
enum CallTariff
{//...
};
unsigned call: 3;
unsigned tariff: 2;
};
現在一個數據從16字節縮減到了8字節,減少了一半,怎么樣,效果還是顯著的吧:)
2. Unions
Unions通過把兩個或更多的數據成員放置在相同地址的內存中來減少內存浪費,這就要求在任何時間只能有一個數據成員有效。Union 可以有成員函數,包括構造函數和析構函數,但是它不能有虛函數。C++支持anonymous unions。anonymous union是一個未命名類型的未命名對象。例如:
union { long n; void * p}; // anonymous
n = 1000L; // members are directly accessed
p = 0; // n is now also 0
不像命名的union,它不能有成員函數以及非public的數據成員。
那么unions什么時候是有用的呢?下面這個類從數據庫中獲取一個人的信息。關鍵字既可以是一個特有的ID或者人名,但是二者卻不能同時有效:
class PersonalDetails
{
private:
char * name;
long ID;
//...
public:
PersonalDetails(const char *nm); //key is of type char * used
PersonalDetails(long id) : ID(id) {} //numeric key used
};
上面這段代碼中就會造成內存的浪費,因為在一個時間只能有一個關鍵字有效。anonymous union可以在這里使用來減少內存的使用,例如:
class PersonalDetails
{
private:
union //anonymous
{
char * name;
long ID;
};
public:
PersonalDetails(const char *nm);
PersonalDetails(long id) : ID(id) {/**/} // direct access to a member
//...
};
通過使用union,PersonalDetails類的大小被減半。但是這里要說明的是,節省4 個字節內存并不值得引入union所帶來的麻煩,除非這個類作為數百萬數據庫記錄的類型或者紀錄在一條很慢的通信線路傳輸。值得注意的是unions并不引入任何運行期負擔,所以這里不會有什么速度上的損失。anonymous union的優點就是它的成員可以被直接訪問。

五. 速度優化
在一些對速度要求非常苛刻的應用系統中,每一個CPU周期都是要爭取的。這個部分展現了一些簡單方法來進行速度優化。
1. 使用類來包裹長的參數列表
一個函數調用的負擔將會隨著參數列表的增長而增加。運行時系統不得不建立堆棧來存儲參數值;通常,當參數很多的時候,這樣一個操作就會花費很長的時間。
把參數列表包裹進一個單獨的類中并且通過引用進行傳遞,這樣將會節省很多的時間。當然,如果函數本身就很長,那么建立堆棧的時間就可以忽略了,因此也就沒有必要這樣做。然而,對于那些執行時間很短而且經常被調用的函數來說,包裹一個長的參數列表在對象中并且通過引用傳遞將會提高性能。
2. 寄存器變量
register specifier被用來告訴編譯器一個對象將被會非常多的使用,可以把它放入寄存器中。例如:
void f()
{
int *p = new int[3000000];
register int *p2 = p; //store the address in a register
for (register int j = 0; j <3000000; j++)
{
*p2++ = 0;
}
//...use p
delete [] p;
}
循環計數是應用寄存器變量的最好的候選者。當它們沒有被存入一個寄存器中,大部分的循環時間都被用在了從內存中取出變量和給變量賦新值上。如果把它存入一個寄存器中的話,將會大大減少這種負擔。需要注意的是,register specifier僅僅是對編譯器的一個建議。就好比內聯函數一樣,編譯器可以拒絕把一個對象存儲到寄存器中。另外,現代的編譯器都會通過把變量放入寄存器中來優化循環計數。Register storage specifier并不僅僅局限在基本類型上,它能夠被應用于任何類型的對象。如果對象太大而不能裝進寄存器的話,編譯器仍然能夠把它放入一個高速存儲器中,例如cache。
用register storage specifier聲明函數型參將會是建議編譯器把實參存入寄存器中而不是堆棧中。例如:

void f(register int j, register Date d);

3. 把那些保持不變的對象聲明為const
通過把對象聲明為const,編譯器就可以利用這個聲明把這樣一個對象放入寄存器中。
4. Virtual function的運行期負擔
當調用一個virtual function,如果編譯器能夠解決調用的靜態化,將不會引入額外的負擔。另外,一個非常短的虛函數可以被內聯處理。在下面這個例子中,一個聰明的編譯器能夠做到靜態調用虛函數:
#include <iostream>
using namespace std;
class V
{
public:
virtual void show() const { cout <<"I"m V"<<endl; }
};
class W : public V
{
public:
void show() const { cout <<"I"m W"<<endl; }
};
void f(V & v, V *pV)
{
v.show();
pV- >show();
}
void g()
{
V v;
f(v, &v);
}
int main()
{
g();
return 0;
}
如果整個程序出現在一個單獨的編譯單元中,編譯器能夠對main()中的g()進行內聯替換。并且在g()中f()的調用也能夠被內聯處理。因為傳給f()的參數的動態類型能夠在編譯期被知曉,因此編譯器能夠把對虛函數的調用靜態化。但是不能保證每個編譯器都這樣做。然而,一些編譯器確實能夠利用在編譯期獲得參數的動態類型從而使得函數的調用在編譯期間就確定了下來,避免了動態綁定的負擔。
5. Function objects VS function pointers
用function objects取代function pointers的好處不僅僅局限在能夠泛化和簡單的維護性上。而且編譯器能夠對function object的函數調用進行內聯處理,從而進一步的增強了性能

六. 最后的求助
迄今為止為大家展示的優化技術并沒有在設計以及代碼的可讀性上做出妥協。事實上,它們中的一些還提高了軟件的穩固性和可維護性。但是在一些對時間和內存有嚴格限制的軟件開發中,上面的技術可能還不夠;有可能還需要一些會影響軟件的可移植性和擴展性的技術。但是這些技術只能在所有其他的優化技術都被應用但是還不符合要求的情況下使用。
1. 關閉RTTI和異常處理支持
當你導入純C代碼給C++編譯器的時候,你可能會發現有一些性能上的損失。這并不是語言或者編譯器的錯誤,而是編譯器作出的一些調整。如果你想獲得和C編譯器同樣的性能,那么請關閉編譯器對RTTI以及異常處理的支持。為什么會這樣呢?因為為了支持RTTI和異常處理,C++編譯器會插入額外的代碼。這樣就增加了可執行體的大小,從而使得效率有所下降。當應用純C代碼的時候,那些額外的代碼是不需要的,所以你可以通過關閉來避免它。
2. 內聯匯編
對時間要求苛刻的部分可以用本地匯編來重寫。結果可能是速度上的顯著提高。然而,這個方法不能想當然的就去實施,因為它將使得將來的修改非常的困難。維護代碼的程序員可能對匯編并不了解。如果想要把軟件運行于其他平臺也需要重寫匯編代碼部分。另外,開發和測試匯編代碼是一件辛苦的工作,它將花費更長的時間。
3. 直接和操作系統進行交互
API函數可以使你直接與操作系統進行交互。有時,直接執行一個系統命令可能會快許多。出于這個目的,你可以使用標準函數system()。例如,在一個dos/windows系統下,你可以這樣顯示當前目錄下的文件:
#include <cstdlib>
using namespace std;
int main()
{
system( "dir"); //execute the "dir" command
}
注意:這里是在速度和可移植性以及可擴展性之間做出的折衷。


5.?

代碼優化

所謂代碼優化是指對程序代碼進行等價(指不改變程序的運行結果)變換。程序代碼可以是中間代碼(如四元式代碼),也可以是目標代碼。等價的含義是使得變換后的代碼運行結果與變換前代碼運行結果相同。優化的含義是最終生成的目標代碼短(運行時間更短、占用空間更小),時空效率優化。原則上,優化可以再編譯的各個階段進行,但最主要的一類是對中間代碼進行優化,這類優化不依賴于具體的計算機。

目錄

分類
要點

編輯本段分類

  編譯過程中可進行的優化可按階段劃分:優化可在編譯的不同階段進行,分為中間代碼一級和目標代碼一級的優化。可按優化涉及的程序范圍劃分:對同一階段,分為局部優化,循環優化和全局優化. 進行優化所需要的基礎是對代碼進行數據流分析和控制流分析。如劃分 DAG,查找循環,分析變量的定值點和引用點等等。最常用的代碼優化技術有刪除多余運算,循環不變代碼外提,強度削弱,變換循環控制條件,合并已知量與復寫傳播,以及刪除無用賦值等等。

編輯本段要點

  一. 盡量采用 div+css布局您的頁面,div+css布局的好處是讓 搜索引擎爬蟲能夠更順利的,更快的,更友好的爬完您的頁面;div+css布局還可以大量縮減網頁大小,使得代碼更簡潔,流暢,更容易放置更多內容。  二. 盡量縮減您的頁面大小,因為搜索引擎爬蟲每次爬行您的站點時,存儲數據的容量有限,一般建議100 KB以下,越小越好,但不能小于5KB。網頁大小減少還有一個好處,能夠促使您的站點形成巨大的內部鏈接網。  三. 盡量少用無用的圖片和flash。內容索引所派出的搜索引擎爬蟲,不認識圖片,只能根據圖片“ ALT, TITLE”等屬性的內容判斷圖片的內容。對于 flash搜索引擎爬蟲更是視而不見。  四. 盡量滿足w3c標準,網頁代碼的編寫滿足W3C標準,能夠提升網站和搜索引擎的友好度,因為搜索引擎收錄標準,排名算法,都是在 W3C標準的基礎上開發的。  五. 盡量更深層次套用標簽 h1、h2、h3、h4、h5…..,讓搜索引擎能夠分辨清晰網頁那一塊很重要,那一塊次之。  六. 盡量少用 JS,JS代碼全部用外部調用文件封裝。搜索引擎不喜歡JS,影響網站的友好度指數。  七. 盡量不使用表格布局,因為搜索引擎對表格布局嵌套3層以內的內容懶的去抓取。搜索引擎爬蟲有時候也是比較懶的,望各位一定要保持代碼和內容在3層以內。  八. 盡量不讓CSS分散在HTML標記里,盡量封裝到外部調用文件。如果CSS出現在 HTML標記里,搜索引擎爬蟲就要分散注意力去關注這些對優化沒有任何意義的東西,所以建議封裝到專用CSS文件中。  九.清理垃圾代碼,要把代碼編輯環境下敲擊鍵盤上的空格鍵所產生的符號;把一些默認屬性代碼,不會影響顯示的代碼;注釋語句如果對代碼可讀性沒有太大影響,清理這些垃圾代碼,會減少不少的空間。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/453858.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/453858.shtml
英文地址,請注明出處:http://en.pswp.cn/news/453858.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

.12-淺析webpack源碼之NodeWatchFileSystem模塊總覽

剩下一個watch模塊&#xff0c;這個模塊比較深&#xff0c;先大概過一下整體涉及內容再分部講解。 流程圖如下&#xff1a; NodeWatchFileSystem const Watchpack require("watchpack");class NodeWatchFileSystem {constructor(inputFileSystem) {this.inputFileSy…

Python 第三方模塊之 beautifulsoup(bs4)- 解析 HTML

簡單來說&#xff0c;Beautiful Soup是python的一個庫&#xff0c;最主要的功能是從網頁抓取數據。官方解釋如下&#xff1a;官網文檔 Beautiful Soup提供一些簡單的、python式的函數用來處理導航、搜索、修改分析樹等功能。 它是一個工具箱&#xff0c;通過解析文檔為用戶提供…

modal vue 關閉_Vue彈出框的優雅實踐

引言頁面引用彈出框組件是經常碰見的需求,如果強行將彈出框組件放入到頁面中,雖然功能上奏效但沒有實現組件與頁面間的解耦,非常不利于后期的維護和功能的擴展.下面舉個例子來說明一下這種做法的弊端.click"openModal()">點擊 :is_open"is_open" close…

Python 第三方模塊之 lxml - 解析 HTML 和 XML 文件

lxml是python的一個解析庫&#xff0c;支持HTML和XML的解析&#xff0c;支持XPath解析方式&#xff0c;而且解析效率非常高 XPath&#xff0c;全稱XML Path Language&#xff0c;即XML路徑語言&#xff0c;它是一門在XML文檔中查找信息的語言&#xff0c;它最初是用來搜尋XML文…

(轉)Linux下PS1、PS2、PS3、PS4使用詳解

Linux下PS1、PS2、PS3、PS4使用詳解 原文&#xff1a;http://www.linuxidc.com/Linux/2016-10/136597.htm 1、PS1——默認提示符 如下所示&#xff0c;可以通過修改Linux下的默認提示符&#xff0c;使其更加實用。在下面的例子中&#xff0c;默認的PS1的值是“\s-\v\$”,顯示出…

開放平臺大抉擇

開放平臺大抉擇之新浪SAE&#xff1a;為個人應用開發帶來福音 導讀&#xff1a;繼上期淘寶網副總裁王文彬從平臺功能特色、運營狀況等多方面分享了淘寶開放平臺的歷程和挑戰之后。國內另一家云平臺服務方的典型代表——Sina App Engine(簡稱SAE)&#xff0c;作為新浪研發中心于…

ip68級防水可以泡多久_iPhone8防水級別升級至IP68:能在1.5米深水中堅持30分鐘

1月15日&#xff0c;業界最新的泄密消息顯示&#xff0c;蘋果擬在今年推出的“iPhone 8”智能手機會是一款革命性的手機&#xff0c;功能和配置就不多說了。蘋果還將解決iPhone 7的一個重要缺陷&#xff0c;就是大大增強iPhone 8的防水性能&#xff0c;防水級別達IP68。《韓國先…

HTTP POST 發送數據的參數 application/x-www-form-urlencoded、multipart/form-data、text/plain

HTTP 簡介 HTTP/1.1 協議規定的 HTTP 請求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。 其中 POST 一般用來向服務端提交數據&#xff0c;本文主要討論 POST 提交數據的幾種方式。 我們知道&#xff0c;HTTP 協議是以 ASCII 碼傳輸&#xff0c;建…

vue 二進制文件的下載(解決亂碼和解壓報錯)

問題描述&#xff1a;項目中使用的是vue框架進行開發&#xff0c;因為文件下載存在權限問題&#xff0c;所以并不能通過 a 鏈接的 href 屬性直接賦值 URL進行下載&#xff0c; &#xff08;如果你的文件沒有下載權限&#xff0c;可以直接通過href屬性賦值URL的方法進行文件下載…

Python 第三方模塊之 psutil - 獲取系統運行的進程和系統利用率信息

一、psutil模塊: 官方網址&#xff1a;http://pythonhosted.org/psutil/ 1.psutil是一個跨平臺庫(http://pythonhosted.org/psutil/)能夠輕松實現獲取系統運行的進程和系統利用率&#xff08;包括CPU、內存、磁盤、網絡等&#xff09;信息。它主要用來做系統監控&#xff0c;…

石頭機器人紅燈快閃_機器人集體“快閃”活動爆紅網絡 “我是AI”與您相約智能新時代...

原標題&#xff1a;機器人集體“快閃”活動爆紅網絡 “我是AI”與您相約智能新時代3月10日下午&#xff0c;天津科學技術館內&#xff0c;悠揚美妙的歌聲《我和我的祖國》突然響起&#xff0c;隨后50個身形矯健的阿爾法機器人伴隨著歌聲翩翩起舞&#xff0c;動作整齊、科技感十…

淺談云計算與數據中心計算

文/林仕鼎 云計算概念發端于Google和Amazon等超大規模的互聯網公司&#xff0c;隨著這些公司業務的成功&#xff0c;作為其支撐技術的云計算也得到了業界的高度認可和廣泛傳播。時至今日&#xff0c;云計算已被普遍認為是IT產業發展的新階段&#xff0c;從而被賦予了很多產業和…

無線網絡實體圖生成工具airgraph-ng

無線網絡實體圖生成工具airgraph-ngairgraph-ng是aircrack-ng套件提供的一個圖表生成工具。該工具可以根據airodump工具生成的CSV文件繪制PNG格式的圖。繪制的圖有兩種類型&#xff0c;分別為AP-客戶端關聯圖和通用探測圖。通過AP-客戶端關聯圖&#xff0c;可以更為直觀的了解無…

高等代數期末考試題庫及答案_數學類高等代數期末考試試題A卷(含答案)

數學類高等代數期末考試試題A卷(含答案)課程編號MTH17063 北京理工大學2010-2011學年第一學期2009級數學類高等代數期末考試試題A卷班級 學號 姓名 成績 一、(25分)設表示域上的所有階矩陣構成的上的線性空間。取定&#xff0c;對于任意的&#xff0c;定義。(1)證明為上的一個線…

cocos2d-lua3.7組件篇(三)-http通信demo

客戶端使用lua、服務端使用QT做為服務器。 步驟&#xff1a; 客戶端 -----------Post 用戶名和密碼 服務端接受Post請求&#xff0c;讀取數據&#xff0c;返回response一、客戶端代碼 loadingImg require"app.scenes.LoadingLayer"local LoginScene class(&qu…

數據挖掘:如何尋找相關項

導讀&#xff1a;隨著大數據時代浪潮的到來數據科學家這一新興職業也越來越受到人們的關注。本文作者Alexandru Nedelcu就將數學挖掘算法與大數據有機的結合起來&#xff0c;并無縫的應用在面臨大數據浪潮的網站之中。 數據科學家需要具備專業領域知識并研究相應的算法以分析對…

Python 第三方模塊之 selenium - 模擬操作 Chrome 瀏覽器

1、安裝selenium 1.1、Python 安裝 selenium 模塊 pip install selenium1.2、下載驅動 選擇和自己chrom版本相對應的驅動到本地&#xff0c;下載地址 http://npm.taobao.org/mirrors/chromedriver/2、Python 操作 from selenium import webdriver import time import json…

jupyter notebook代碼導出_Jupyter Notebook導出包含中文的pdf_親測有效

Jupyter Notebook是很好的數據科學創作環境&#xff0c;是非常方便的Python代碼編輯器。jupyter提供導出的格式有.py、.html、.md、.pdf等。目前用其導出包含中文的pdf會遇到很多坑&#xff0c;網上也有一些解決方案&#xff0c;大致分為兩種方式&#xff0c;一是安裝 pandoc并…

前端之使用 POST 提交數據并跳轉

GET 方式 window.location.href是我們常用來在js中實現頁面跳轉的方法&#xff0c;這是使用get方式發送請求&#xff0c;示例如下 window.location.href url;優點是簡單易用&#xff0c;缺點是如果有參數的話&#xff0c;參數會暴露在url地址中&#xff0c;這降低了系統的安…

cef js返回c++的代碼_CEF3開發者系列之外篇——IE中JS與C++交互

使用IE內核開發客戶端產品&#xff0c;系統和前端頁面之間的交互&#xff0c;通常給開發和維護帶來很大的便利性。但操作系統和前端之間的交互卻是比較復雜的。具體來說就是腳本語言和編譯語言的交互。在IE內核中html和css雖然不兼容,但是IE編程接口是完全一樣的,這得益于微軟的…