(轉)C代碼優化方案

C代碼優化方案

原文地址:http://www.uml.org.cn/c++/200811103.asphtml

 

目錄c++

C代碼優化方案程序員

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

二、使用盡可能小的數據類型編程

三、減小運算的強度數組

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

    (2)求餘運算數據結構

    (3)平方運算模塊化

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

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

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

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

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

四、結構體成員的佈局

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

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

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

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

五、循環優化

    (1)充分分解小的循環

    (2)提取公共部分

    (3)延時函數

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

    (5)循環展開

    (6)循環嵌套

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

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

    (9)循環轉置

    (10)公用代碼塊

    (11)提高循環的性能

    (12)選擇好的無限循環

六、提升CPU的並行性

    (1)使用並行代碼

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

七、循環不變計算

八、函數

    (1)Inline函數

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

    (3)減小函數調用參數

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

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

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

九、採用遞歸

十、變量

    (1)register變量

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

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

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

十一、使用嵌套的if結構

 

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

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

 

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

	數組索引						指針運算
	for(;;){						p=array
		A=array[t++];				for(;;){
										a=*(p++);
		。。。。。。。。。				。。。。。。
	}								}

 

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

 

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

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

 

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

 

三、減小運算的強度

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

 

四、結構體成員的佈局

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

 

(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;
}

 

五、循環優化

(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 < 10;)
{
	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)好。

 

六、提升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;
} 

 

七、循環不變計算

對於一些不須要循環變量參加運算的計算任務能夠把它們放到循環外面,如今許多編譯器仍是能本身幹這件事,不過對於中間使用了變量的算式它們就不敢動了,因此不少狀況下你還得本身幹。對於那些在循環中調用的函數,凡是不必執行屢次的操做統統提出來,放到一個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一次,而後將結果代入第二個式子,節約了一次除法運算。

 

八、函數優化

(1)Inline函數

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

 

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

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

 

(3)減小函數調用參數

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

 

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

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

 

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

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

 

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

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

 

九、採用遞歸

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

 

十、變量

(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)在循環開始前聲明變量

 

十一、使用嵌套的if結構

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

 

 

說明:

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

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

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

相關文章
相關標籤/搜索