編寫高效的C程序與C代碼優化

2021-03-03 程序猿

說明:

本篇文章翻譯自:http://www.codeproject.com/Articles/6154/Writing-Efficient-C-and-C-Code-Optimization

其中參考了文章:http://www.cppblog.com/xlander/archive/2006/07/21/10289.html

但文章中的代碼格式沒有排版,不方便查看,而且有部分翻譯錯誤以及其他錯誤,這篇文章除了參考原文和譯文,也加入了自己的一些理解和代碼,雖然是一篇2006年的文章,但是其中的一些技巧還是挺值得學習的,特重新整理出來與大家分享。

雖然對於優化C代碼有很多有效的指導方針,但是對於徹底地了解編譯器和你工作的機器依然無法取代,通常,加快程序的速度也會加大代碼量。這些增加的代碼也會影響一個程序的複雜度和可讀性,這是不可接受的,比如你在一些小型的設備上編程,例如:行動裝置、PDA……,這些有著嚴格的內存限制,於是,在優化的座右銘是:寫代碼在內存和速度都應該優化。

整型數 / Integers

在我們知道使用的數不可能是負數的時候,應該使用unsigned int取代int,一些處理器處理整數算數運算的時候unsigned int比int快,於是,在一個緊緻的循環裡面定義一個整型變量,最好這樣寫代碼:

register unsigned int variable_name;

然而,我們不能保證編譯器會注意到那個register關鍵字,也有可能,對某種處理器來說,有沒有unsigned是一樣的。這兩個關鍵字並不是可以在所有的編譯器中應用。記住,整形數運算要比浮點數運算快得多,因為處理器可以直接進行整型數運算,浮點數運算需要依賴於外部的浮點數處理器或者浮點數數學庫。我們處理小數的時候要精確點些(比如我們在做一個簡單的統計程序時),要限制結果不能超過100,要儘可能晚的把它轉化成浮點數。

除法和餘數 / Division and Remainder

在標準的處理器中,根據分子和分母的不同,一個32位的除法需要20-140個時鐘周期來執行完成,等於一個固定的時間加上每個位被除的時間。

Time (分子/ 分母) = C0 + C1* log2 (分子/分母)

= C0 + C1 * (log2 (分子) - log2 (分母)).

現在的ARM處理器需要消耗20+4.3N個時鐘周期,這是一個非常費時的操作,要儘可能的避免。在有些情況下,除法表達式可以用乘法表達是來重寫。比方說,(a/b)>c可以寫成a>(c*b),條件是我們已經知道b為非負數而且b*c不會超過整型數的取值範圍。如果我們能夠確定其中的一個操作數為unsigned,那麼使用無符號除法將會更好,因為它要比有符號除法快得多。

合併除法運算和取餘運算 / Combining division and remainder

在一些情況下,除法運算和取餘運算都需要用到,在這種情況下,編譯器會將除法運算和取餘運算合併,因為除法運算總是同時返回商和餘數。如果兩個運算都要用到,我們可以將他們寫到一起

int func_div_and_mod (int a, int b) { return (a / b) + (a % b);}

除數是2的冪的除法和取餘 / Division and remainder by powers of two

如果除法運算中的除數是2的冪,我們對這個除法運算還可以進一步優化,編譯器會使用移位運算來進行這種除法運算。所以,我們要儘可能調整比例為2的冪(比方說要用64而不用66)。如果是無符號數,它要比有符號的除法快得多。

typedef unsigned int uint;

uint div32u (uint a) {

return a / 32;

}

int div32s (int a) {

return a / 32;

}

這兩種除法都會避免調用除法函數,另外,無符號的除法要比有符號的除法使用更少的指令。有符號的除法要耗費更多的時間,因為這種除法是使最終結果趨向於零的,而移位則是趨向於負無窮。

取模運算的替換 / An alternative for modulo arithmetic

我們一般使用取餘運算進行取模,不過,有時候使用 if 語句來重寫也是可行的。考慮下面的兩個例子:

uint modulo_func1 (uint count)

{

return (++count % 60);

}

uint modulo_func2 (uint count)

{

if (++count >= 60)

count = 0;

return (count);

}

第二個例子要比第一個更可取,因為由它產生的代碼會更快,注意:這只是在count取值範圍在0 – 59之間的時候才行。

但是我們可以使用如下的代碼(筆者補充)實現等價的功能:

uint modulo_func3 (uint count)

{

if (++count >= 60)

count %= 60;

return (count);

}

使用數組索引 / Using array indices

假設你要依據某個變量的值,設置另一個變量的取值為特定的字符,你可能會這樣做:

switch(queue) {

case 0 : letter = 'W';

break;

case 1 : letter = 'S';

break;

case 2 : letter = 'U';

break;

}

或者這樣:

if(queue == 0)

letter = 'W';

else if ( queue == 1 )

letter = 'S';

else

letter = 'U';

有一個簡潔且快速的方式是簡單的將變量的取值做成一個字符串索引,例如:

static char *classes = "WSU";

letter = classes[queue];

全局變量 / Global variables

全局變量不會被分配在寄存器上,修改全局變量需要通過指針或者調用函數的方式間接進行。所以編譯器不會將全局變量存儲在寄存器中,那樣會帶來額外的、不必要的負擔和存儲空間。所以在比較關鍵的循環中,我們要不使用全局變量。

如果一個函數要頻繁的使用全局變量,我們可以使用局部變量,作為全局變量的拷貝,這樣就可以使用寄存器了。條件是本函數調用的任何子函數不使用這些全局變量。

舉個例子:

int f(void);

int g(void);

int errs;

void test1(void)

{

errs += f();

errs += g();

}

void test2(void)

{

int localerrs = errs;

localerrs += f();

localerrs += g();

errs = localerrs;

}

可以看到test1()中每次加法都需要讀取和存儲全局變量errs,而在test2()中,localerrs分配在寄存器上,只需要一條指令。

使用別名 / Using Aliases

考慮下面的例子:

void func1( int *data )

{

int i;

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

anyfunc(*data, i);

}

即使*data從來沒有變化,編譯器卻不知道anyfunc()沒有修改它,於是程序每次用到它的時候,都要把它從內存中讀出來,可能它只是某些變量的別名,這些變量在程序的其他部分被修改。如果能夠確定它不會被改變,我們可以這樣寫:

void func1( int *data )

{

int i;

int localdata;

localdata = *data;

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

anyfunc(localdata, i);

}

這樣會給編譯器優化工作更多的選擇餘地。

活躍變量和洩漏 / Live variables and spilling

寄存器的數量在每個處理器當中都是固定的,所以在程序的某個特定的位置,可以保存在寄存器中的變量的數量是有限制的。有些編譯器支持「生命周期分割」(live-range splitting),也就是說在函數的不同部分,變量可以被分配到不同的寄存器或者內存中。變量的生存範圍被定義成:起點是對該變量的一次空間分配,終點是在下次空間分配之前的最後一次使用之間。在這個範圍內,變量的值是合法的,是活的。在生存範圍之外,變量不再被使用,是死的,它的寄存器可以供其他變量使用,這樣,編譯器就可以安排更多的變量到寄存器當中。

可分配到寄存器的變量需要的寄存器數量等於經過生命範圍重疊的變量的數目,如果這個數目超過可用的寄存器的數量,有些變量就必須被暫時的存儲到內存中。這種處理叫做「洩漏(spilling)」。

編譯器優先釋放最不頻繁使用的變量,將釋放的代價降到最低。可以通過以下方式避免變量的「釋放」:

●限制活躍變量的最大數目:通常可以使用簡單小巧的表達式,在函數內部不使用太多的變量。把大的函數分割成更加簡單的、更加小巧的多個函數,也可能會有所幫助。

●使用關鍵字register修飾最經常使用的變量:告訴編譯器這個變量將會被經常用到,要求編譯器使用非常高的優先級將此變量分配到寄存器中。儘管如此,在某些情況下,變量還是可能被洩漏。

變量類型 / Variable Types

C編譯器支持基本的變量類型:char、short、int、long(signed、unsigned)、float、double。為變量定義最恰當的類型,非常重要,因為這樣可以減少代碼和數據的長度,可以非常顯著的提高效率。

局部變量 / Local variables

如果可能,局部變量要避免使用char和short。對於char和short類型,編譯器在每次分配空間以後,都要將這種局部變量的尺寸減少到8位或16位。這對於符號變量來說稱為符號擴展,對無符號變量稱為無符號擴展。這種操作是通過將寄存器左移24或16位,然後再有符號(或無符號的)右移同樣的位數來實現的,需要兩條指令(無符號字節變量的無符號擴展需要一條指令)。

這些移位操作可以通過使用int和unsigned int的局部變量來避免。這對於那些首先將數據調到局部變量然後利用局部變量進行運算的情況尤其重要。即使數據以8位或16位的形式輸入或輸出,把他們當作32位來處理仍是有意義的。

我們來考慮下面的三個例子函數:

int wordinc (int a)

{

return a + 1;

}

short shortinc (short a)

{

return a + 1;

}

char charinc (char a)

{

return a + 1;

}

他們的運算結果是相同的,但是第一個代碼片斷要比其他片斷運行的要快。

指針 / Pointers

如果可能,我們應該使用結構體的引用作為參數,也就是結構體的指針,否則,整個結構體就會被壓入堆棧,然後傳遞,這會降低速度。程序適用值傳遞可能需要幾K字節,而一個簡單的指針也可以達到同樣的目的,只需要幾個字節就可以了。

如果在函數內部不會改變結構體的內容,那麼就應該將參數聲明為const型的指針。舉個例子:

void print_data_of_a_structure (const Thestruct *data_pointer)

{

...printf contents of the structure...

}

這個例子代碼告知編譯器在函數內部不會改變外部結構體的內容,訪問他們的時候,不需要重讀。還可以確保編譯器捕捉任何修改這個只讀結構體的代碼,給結構體以額外的保護。

指針鏈 / Pointer chains

指針鏈經常被用來訪問結構體的信息,比如,下面的這段常見的代碼:

typedef struct { int x, y, z; } Point3;

typedef struct { Point3 *pos, *direction; } Object;

void InitPos1(Object *p)

{

p->pos->x = 0;

p->pos->y = 0;

p->pos->z = 0;

}

代碼中,處理器在每次賦值操作的時候都要重新裝載p->pos,因為編譯器不知道p->pos->x不是p->pos的別名。更好的辦法是將p->pos緩存成一個局部變量,如下:

void InitPos2(Object *p)

{

Point3 *pos = p->pos;

pos->x = 0;

pos->y = 0;

pos->z = 0;

}

另一個可能的方法是將Point3結構體包含在Object結構體中,完全避免指針的使用。

條件的執行 / Conditional Execution

條件執行主要用在if語句中,同時也會用到由關係運算(<,==,>等)或bool運算(&&, !等)組成的複雜的表達式。儘可能的保持if和else語句的簡單是有好處的,這樣才能很好的條件化。關係表達式應該被分成包含相似條件的若干塊。

下面的例子演示了編譯器如何使用條件執行:

int g(int a, int b, int c, int d)

{

if(a > 0 && b > 0 && c < 0 && d < 0) //分組化的條件被捆綁在一起

return a + b + c + d;

return -1;

}

條件被分組,便以其能夠條件化他們。

Boolean表達式和範圍檢查 / Boolean Expressions & Range checking

有一種常見的boolean表達式被用來檢查是否一個變量取值在某個特定的範圍內,比方說,檢查一個點是否在一個窗口內。

bool PointInRectangelArea (Point p, Rectangle *r)

{

return (p.x >= r->xmin && p.x < r->xmax && p.y >= r->ymin && p.y < r->ymax);

}

這裡還有一個更快的方法:把(x >= min && x < max) 轉換成 (unsigned)(x-min) < (max-min). 尤其是min為0時,更為有效。下面是優化後的代碼:

bool PointInRectangelArea (Point p, Rectangle *r)

{

return ((unsigned) (p.x - r->xmin) < r->xmax && (unsigned) (p.y - r->ymin) < r->ymax);

}

Boolean表達式&與零的比較 / Boolean Expressions & Compares with zero

在比較(CMP)指令後,相應的處理器標誌位就會被設置。這些標誌位也可以被其他的指令設置,諸如MOV, ADD, AND, MUL, 也就是基本的數學和邏輯運算指令(數據處理指令)。假如一條數據處理指令要設置這些標誌位,那麼N和Z標誌位的設置方法跟把數字和零比較的設置方法是一樣的。N標誌位表示結果是不是負數,Z標誌位表示結果是不是零。

在C語言中,處理器中的N和Z標誌位對應的有符號數的關係運算符是x < 0, x >= 0, x == 0, x != 0,無符號數對應的是x == 0, x != 0 (or x > 0)。

C語言中,每用到一個關係運算符,編譯器就會產生一個比較指令。如果關係運算符是上面的其中一個,在數據處理指令緊跟比較指令的情況下,編譯器就會將比較指令優化掉。比如:

int aFunction(int x, int y)

{

if (x + y < 0)

return 1;

else

return 0;

}

這樣做,會在關鍵循環中節省比較指令,使代碼長度減少,效率增加。C語言中沒有借位(carry)標誌位和溢出(overflow)標誌位的概念,所以如果不使用內嵌彙編語言,要訪問C和V標誌位是不可能的。儘管如此,編譯器支持借位標誌位(無符號數溢出),比方說:

int sum(int x, int y)

{

int res;

res = x + y;

if ((unsigned) res < (unsigned) x) // carry set? //

res++;

return res;

}

惰性評估計算 / Lazy Evaluation Exploitation

在類似與這樣的 if(a>10 && b=4) 語句中, 確保AND表達式的第一部分最有可能為false, 結果第二部分極有可能不被執行.

用switch() 代替if...else...,在條件選擇比較多的情況下,可以用if…else…else…,像這樣:

if( val == 1)

dostuff1();

else if (val == 2)

dostuff2();

else if (val == 3)

dostuff3();

使用switch可以更快:

switch( val )

{

case 1: dostuff1(); break;

case 2: dostuff2(); break;

case 3: dostuff3(); break;

}

在if語句中,即使是最後一個條件成立,也要先判斷所有前面的條件是否成立。Switch語句能夠去除這些額外的工作。如果你不得不使用if…else,那就把最可能的成立的條件放在前面。

二分分解 / Binary Breakdown

把判斷條件做成二進位的風格,比如,不要使用下面的列表:

if(a == 1) { } else if(a == 2) { } else if(a == 3) { } else if(a == 4) { } else if(a == 5) { } else if(a == 6) { } else if(a == 7) { } else if(a == 8) { }}

而採用:

if(a <= 4) { if(a == 1) { } else if(a == 2) { } else if(a == 3) { } else if(a == 4) { } } else { if(a == 5) { } else if(a == 6) { } else if(a == 7) { } else if(a == 8) { } }

甚至:

if(a <= 4) {

if(a <= 2) {

if(a == 1) {

/* a is 1 */

} else {

/* a must be 2 */

}

} else {

if(a == 3) {

/* a is 3 */

} else {

/* a must be 4 */

}

}

} else {

if(a <= 6) {

if(a == 5) {

/* a is 5 */

} else {

/* a must be 6 */

}

} else {

if(a == 7) {

/* a is 7 */

} else {

/* a must be 8 */

}

}

}

慢速、低效:

c = getch();

switch(c){

case 'A': {

do something;

break;

}

case 'H': {

do something;

break;

}

case 'Z': {

do something;

break;

}

}

快速、高效:

c = getch();

switch(c) {

case 0: {

do something;

break;

}

case 1: {

do something;

break;

}

case 2: {

do something;

break;

}

}

以上是兩個case語句之間的比較

switch語句和查找表 / Switch statement vs. lookup tables

switch語句通常用於以下情況:

●調用幾個函數中的一個

●設置一個變量或返回值

●執行幾個代碼片斷中的一個

如果case表示是密集的,在使用switch語句的前兩種情況中,可以使用效率更高的查找表。比如下面的兩個實現彙編代碼轉換成字符串的例程:

char * Condition_String1(int condition) {

switch(condition) {

case 0: return "EQ";

case 1: return "NE";

case 2: return "CS";

case 3: return "CC";

case 4: return "MI";

case 5: return "PL";

case 6: return "VS";

case 7: return "VC";

case 8: return "HI";

case 9: return "LS";

case 10: return "GE";

case 11: return "LT";

case 12: return "GT";

case 13: return "LE";

case 14: return "";

default: return 0;

}

}

char * Condition_String2(int condition) {

if((unsigned) condition >= 15) return 0;

return

"EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" +

3 * condition;

}

第一個例程需要240個字節,第二個只需要72個。

循環終止 / Loop termination

如果不加留意地編寫循環終止條件,就可能會給程序帶來明顯的負擔。我們應該儘量使用「倒數到零」的循環,使用簡單的循環終止條件。循環終止條件相對簡單,程序在執行的時候也會消耗相對少的時間。拿下面兩個計算n!的例子來說,第一個例子使用遞增循環,第二個使用遞減循環。

int fact1_func (int n)

{

int i, fact = 1;

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

fact *= i;

return (fact);

}

int fact2_func(int n)

{

int i, fact = 1;

for (i = n; i != 0; i--)

fact *= i;

return (fact);

}

結果是,第二個例子要比第一個快得多。

更快的for()循環 / Faster for() loops

這是一個簡單而有效的概念,通常情況下,我們習慣把for循環寫成這樣:

for( i = 0; i < 10; i++){ ... }

i 值依次為:0,1,2,3,4,5,6,7,8,9

在不在乎循環計數器順序的情況下,我們可以這樣:

for( i = 10; i--; ) { ... }

i值依次為: 9,8,7,6,5,4,3,2,1,0,而且循環要更快

這種方法是可行的,因為它是用更快的i--作為測試條件的,也就是說「i是否為非零數,如果是減一,然後繼續」。相對於原先的代碼,處理器不得不「把i減去10,結果是否為非零數,如果是,增加i,然後繼續」,在緊密循環(tight loop)中,這會產生顯著的區別。

這種語法看起來有一點陌生,卻完全合法。循環中的第三條語句是可選的(無限循環可以寫成這樣for(;;)),下面的寫法也可以取得同樣的效果:

for(i = 10; i; i--){}

或者:

for(i = 10; i != 0; i--){}

我們唯一要小心的地方是要記住循環需要停止在0(如果循環是從50-80,這樣做就不行了),而且循環的計數器為倒計數方式。

另外,我們還可以把計數器分配到寄存器上,可以產生更為有效的代碼。這種將循環計數器初始化成循環次數,然後遞減到零的方法,同樣適用於while和do語句。

混合循環/ Loop jamming

在可以使用一個循環的場合,決不要使用兩個。但是如果你要在循環中進行大量的工作,超過處理器的指令緩衝區,在這種情況下,使用兩個分開的循環可能會更快,因為有可能這兩個循環都被完整的保存在指令緩衝區裡了。

//原先的代碼

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

stuff();

}

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

morestuff();

}

//更好的做法

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

stuff();

morestuff();

}

函數循環 / Function Looping

調用函數的時候,在性能上就會付出一定的代價。不光要改變程序指針,還要將那些正在使用的變量壓入堆棧,分配新的變量空間。為了提高程序的效率,在程序的函數結構上,有很多工作可以做。保證程序的可讀性的同時,還要儘量控制程序的大小。

如果一個函數在一個循環中被頻繁調用,就可以考慮將這個循環放在函數的裡面,這樣可以免去重複調用函數的負擔,比如:

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

{

func(t,i);

}

void func(int w, d)

{

lots of stuff.

}

可以寫成:

func(t);

void func(w)

{

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

//lots of stuff.

}

}

展開循環 / Loop unrolling

為了提高效率,可以將小的循環解開,不過這樣會增加代碼的尺寸。循環被拆開後,會降低循環計數器更新的次數,減少所執行的循環的分支數目。如果循環只重複幾次,那它完全可以被拆解開,這樣,由循環所帶來的額外開銷就會消失。

比如:

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

something(i);

}

//更高效的方式:

something(0);

something(1);

something(2);

因為在每次的循環中,i 的值都會增加,然後檢查是否有效。編譯器經常會把這種簡單的循環解開,前提是這些循環的次數是固定的。對於這樣的循環:

for(i = 0; i < limit; i++) { ... }

就不可能被拆解,因為我們不知道它循環的次數到底是多少。不過,將這種類型的循環拆解開並不是不可能的。

與簡單循環相比,下面的代碼的長度要長很多,然而具有高得多的效率。選擇8作為分塊大小,只是用來演示,任何合適的長度都是可行的。例子中,循環的成立條件每八次才被檢驗一次,而不是每次都要檢驗。如果需要處理的數組的大小是確定的,我們就可以使用數組的大小作為分塊的大小(或者是能夠整除數組長度的數值)。不過,分塊的大小跟系統的緩存大小有關。

#include<stdio.H>

#define BLOCKSIZE (8)

int main(void)

{

int i = 0;

int limit = 33; /* could be anything */

int blocklimit;

/* The limit may not be divisible by BLOCKSIZE,

go as near as we can first, then tidy up.

*/

blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;

/* unroll the loop in blocks of 8 */

while(i < blocklimit) {

printf("process(%d)\n", i);

printf("process(%d)\n", i+1);

printf("process(%d)\n", i+2);

printf("process(%d)\n", i+3);

printf("process(%d)\n", i+4);

printf("process(%d)\n", i+5);

printf("process(%d)\n", i+6);

printf("process(%d)\n", i+7);

/* update the counter */

i += 8;

}

/*

* There may be some left to do.

* This could be done as a simple for() loop,

* but a switch is faster (and more interesting)

*/

if( i < limit )

{

/* Jump into the case at the place that will allow

* us to finish off the appropriate number of items.

*/

switch( limit - i )

{

case 7 : printf("process(%d)\n", i); i++;

case 6 : printf("process(%d)\n", i); i++;

case 5 : printf("process(%d)\n", i); i++;

case 4 : printf("process(%d)\n", i); i++;

case 3 : printf("process(%d)\n", i); i++;

case 2 : printf("process(%d)\n", i); i++;

case 1 : printf("process(%d)\n", i);

}

}

return 0;

}

計算非零位的個數 / counting the number of bits set

例1:測試單個的最低位,計數,然後移位。

//example1

int countbit1(uint n)

{

int bits = 0;

while (n != 0) {

if(n & 1) bits++;

n >>= 1;

}

return bits;

}

例2:先除4,然後計算被4處的每個部分。循環拆解經常會給程序優化帶來新的機會。

//example - 2

int countbit2(uint n)

{

int bits = 0;

while (n != 0) {

if (n & 1) bits++;

if (n & 2) bits++;

if (n & 4) bits++;

if (n & 8) bits++;

n >>= 4;

}

return bits;

}

儘早地退出循環 / Early loop breaking

通常沒有必要遍歷整個循環。舉例來說,在數組中搜索一個特定的值,我們可以在找到我們需要值之後立刻退出循環。下面的例子在10000個數字中搜索-99。

found = FALSE;

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

{

if(list[i] == -99) {

found = TRUE;

}

}

if(found) printf("Yes, there is a -99. Hooray!\n");

這樣做是可行的,但是不管這個被搜索到的項目出現在什麼位置,都會搜索整個數組。跟好的方法是,再找到我們需要的數字以後,立刻退出循環。

found = FALSE;

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

{

if( list[i] == -99 ) {

found = TRUE;

break;

}

}

if( found ) printf("Yes, there is a -99. Hooray!\n");

如果數字出現在位置23上,循環就會終止,忽略剩下的9977個。

函數設計 / Function Design

保持函數短小精悍,是對的。這可以使編譯器能夠跟高效地進行其他的優化,比如寄存器分配。

調用函數的開銷 / Function call overhead

對處理器而言,調用函數的開銷是很小的,通常,在被調用函數所進行的工作中,所佔的比例也很小。能夠使用寄存器傳遞的函數參數個數是有限制的。這些參數可以是整型兼容的(char,short,int以及float都佔用一個字),或者是4個字以內的結構體(包括2個字的double和long long)。假如參數的限制是4,那麼第5個及後面的字都會被保存到堆棧中。這會增加在調用函數是存儲這些參數的,以及在被調用函數中恢復這些參數的代價。

int f1(int a, int b, int c, int d) {

return a + b + c + d;

}

int g1(void) {

return f1(1, 2, 3, 4);

}

int f2(int a, int b, int c, int d, int e, int f) {

return a + b + c + d + e + f;

}

ing g2(void) {

return f2(1, 2, 3, 4, 5, 6);

}

g2函數中,第5、6個參數被保存在堆棧中,在f2中被恢復,每個參數帶來2次內存訪問。

最小化參數傳遞的開銷 / Minimizing parameter passing overhead

為了將傳遞參數給函數的代價降至最低,我們可以:

儘可能確保函數的形參不多於四個,甚至更少,這樣就不會使用堆棧來傳遞參數。

如果一個函數形參多於四個,那就確保在這個函數能夠做大量的工作,這樣就可以抵消由傳遞堆棧參數所付出的代價。

用指向結構體的指針作形參,而不是結構體本身。

把相關的參數放到一個結構裡裡面,然後把它的指針傳給函數,可以減少參數的個數,增加程序的可讀性。

將long類型的參數的個數降到最小,因為它使用兩個參數的空間。對於double也同樣適用。

避免出現參數的一部分使用寄存器傳輸,另一部分使用堆棧傳輸的情況。這種情況下參數將被全部壓到堆棧裡。

避免出現函數的參數個數不定的情況。這種情況下,所有參數都使用堆棧。

葉子函數 / Leaf functions

如果一個函數不再調用其他函數,這樣的函數被稱為葉子函數。在許多應用程式中,大約一半的函數調用是對葉子函數的調用。葉子函數在所有平臺上都可以得到非常高效的編譯,因為他們不需要進行參數的保存和恢復。在入口壓棧和在出口退棧的代價,跟一個足夠複雜的需要4個或者5個參數的葉子函數所完成的工作相比,是非常小的。如果可能的話,我們就要儘量安排經常被調用的函數成為葉子函數。函數被調用的次數可以通過模型工具(profiling facility)來確定。這裡有幾種方法可以確保函數被編譯成葉子函數:

●不調用其他函數:包括那些被轉換成調用C語言庫函數的運算,比如除法、浮點運算。

●使用關鍵字__inline修飾小的函數。

內聯函數 / Inline functions

對於所有調試選項,內嵌函數是被禁止的。使用inline關鍵字修飾函數後,跟普通的函數調用不同,代碼中對該函數的調用將會被函數體本身代替。這會使代碼更快,另一方面它會影響代碼的長度,尤其是內嵌函數比較大而且經常被調用的情況下。

__inline int square(int x) {

return x * x;

}

#include <MATH.H>

double length(int x, int y){

return sqrt(square(x) + square(y));

}

使用內嵌函數有幾個優點:

●沒有調用函數的開銷。

因為函數被直接代替,沒有任何額外的開銷,比如存儲和恢復寄存器。

●更低的參數賦值開銷。

參數傳遞的開銷通常會更低,因為它不需要複製變量。如果其中一些參數是常量,編譯器還可以作進一步的優化。

內嵌函數的缺點是如果函數在許多地方被調用,將會增加代碼的長度。長度差別的大小非常依賴於內嵌函數的大小和調用的次數。

僅將少數關鍵函數設置成內嵌函數是明智的。如果設置得當,內嵌函數可以減少代碼的長度,一次函數調用需要一定數量的指令,但是,使用優化過的內嵌函數可以編譯成更少的指令。

使用查找表 / Using Lookup Tables

有些函數可以近似成查找表,這樣可以顯著的提高效率。查找表的精度一般比計算公式的精度低,不過在大多數程序中,這種精度就足夠了。

許多信號處理軟體(比如MODEM調製軟體)會大量的使用sin和cos函數,這些函數會帶來大量的數學運算。對於實時系統來說,精度不是很重要,sin/cos查找表顯得更加實用。使用查找表的時候,儘量將相近的運算合併成一個查找表,這樣要比使用多個查找表要更快和使用更少的空間。

浮點運算 / Floating-Point Arithmetic

儘管浮點運算對於任何處理器來講都是很費時間的,有的時候,我們還是不得不用到浮點運算,比方說實現信號處理。儘管如此,編寫浮點運算代碼的時候,我們要牢記:

●浮點除法是慢的

除法要比加法或者乘法慢兩倍,我們可以把被一個常數除的運算寫成被這個數的倒數乘(比如,x=x/3.0寫成x=x*(1.0/3.0))。倒數的計算在編譯階段就被完成。

●使用float代替double

Float型變量消耗更少的內存和寄存器,而且因為它的低精度所以具有更高的效率。在精度足夠的情況下,就要使用float。

●不要使用先驗函數(transcendental functions),

先驗函數(比如sin,cos,log)是通過使用一系列的乘法和加法實現的,所以這些運算會比普通的乘法慢10倍以上。

●簡化浮點表達式

編譯器在整型跟浮點型混合的運算中不會進行太多的優化。比如3 * (x / 3) 不會被優化成x,因為浮點運算通常會導致精度的降低,甚至表達式的順序都是重要的: (a + b) + c 不等於 a + (b + c)。因此,進行手動的優化是有好處的。

不過,在特定的場合下,浮點運算的效率達不到指定的水平,這種情況下,最好的辦法可能是放棄浮點運算,轉而使用定點運算。當變量的變化範圍足夠的小,定點運算要比浮點運算精度更高、速度更快。

其他的技巧 / Misc tips

一般情況下,可以用存儲空間換取時間。你可以緩存那些經常用到的數據,而不是每次都重新計算、或者重新裝載。比如sin/cos表,或者偽隨機數的表(如果你不是真的需要隨機數,你可以在開始的時候計算1000個,在隨後的代碼中重複利用就是了)

儘量少的使用全局變量。

將一個文件內部的變量聲明成靜態的,除非它有必要成為全局的。

不要使用遞歸。遞歸可以使代碼非常整齊和美觀,但會產生大量的函數調用和開銷。

訪問單維數組要比多維數組快

使用#defined宏代替經常用到的小函數。

引用/References

●Writing Efficient C for ARM

●Document number: ARM DAI 0034A

●Issued: January 1998

●Copyright Advanced RISC Machines Ltd. (ARM) 1998

●Richard's C Optimization page OR: How to make your C, C++ or Java program run faster with little effort.

●Code Optimization Using the GNU C Compiler By Rahul U Joshi.

●Compile C Faster on Linux [Christopher W. Fraser (Microsoft Research), David R. Hanson (Princeton University)]

●CODE OPTIMIZATION - COMPILER [1] [2]

[Thanks to Craig Burley for the excellent comments. Thanks to Timothy Prince for the note on architectures with Instruction Level Parallelism].

●An Evolutionary Analysis of GNU C Optimizations [Using Natural Selection to Investigate Software Complexities by Scott Robert Ladd. Updated: 16 December 2003]

其他網絡資源 / Other URLs

http://www.xs4all.nl/~ekonijn/loopy.html

http://www.public.asu.edu/~sshetty/Optimizing_Code_Manual.doc
http://www.abarnett.demon.co.uk/tutorial.html

來自:codingwu-博客園

連結:http://www.cnblogs.com/archimedes/p/writing-efficient-c-and-code-optimization.html

—————————————————

●本文編號630,以後想閱讀這篇文章直接輸入630即可。

●本文分類「C」,搜索分類名可以獲得相關文章。

●輸入m可以獲取到全部文章目錄

●輸入r可以獲取到熱門文章推薦

●輸入f可以獲取到全部分類名稱

—————————————————

小猿個人微信:itcodemonkey 歡迎調戲


推薦一個微信公眾號:IT電商網,長按下面的微信號可以進行複製

itdianshang


點擊「閱讀原文」可關注


相關焦點

  • 高效的C編程之:C編譯器及其優化
    本文引用地址:http://www.eepw.com.cn/article/257024.htm本章將幫助讀者在ARM處理器上編寫高效的C代碼。
  • 【C語言】02.第一個C語言程序
    學習C語言語法的目的:就是能夠利用C語言編寫程序,然後運行程序跟硬體(計算機、手機等硬體設備)進行交互。由於我們的最終目的是學習iOS開發,學習iOS開發的話必須在Mac系統下,因此我就在Mac系統環境下開發C語言程序,而不是在Windows環境下。
  • C語言編寫程序求水仙花數
    C語言編寫程序求水仙花數水仙花數是一個數學問題,其實質是一個三位數,個位數的立方加十位數的立方加百位數的立方之和等於這個三位數本身。例如153=1*1*1+5*5*5+3*3*3,即153=1+125+27。
  • 在Stata中編寫估計命令:編寫C語言插件
    這篇文章演示了如何用其他語言(如C,C 或Java)編寫的代碼插入到Stata中。這種技術被稱為Stata編寫插件或編寫動態連結庫(DLL)。本文中,在C語言中編寫一個插件,它實現了mymean11.ado中mymean_work()執行的計算,在文章在Stata中編寫估計命令編寫插件中討論過。
  • Code::Blocks使用教程(使用Code::Blocks編寫C語言程序)
    CodeBlocks 完全支持單個源文件的編譯,如果你的程序只有一個源文件(初學者基本上都是在單個源文件下編寫代碼),那麼不用創建項目,直接運行即可;如果有多個源文件,才需要創建項目。注意:保存時,將源文件後綴名改為 .c。2) 生成可執行程序在上方菜單欄中選擇 構建 --> 構建,就可以完成 hello.c 的編譯工作。
  • 嵌入式平臺ARM的C代碼優化方法
    本文介紹了ARM平臺的C代碼優化方法,從數據類型選擇、數據結構組織、局部變量選擇、函數inline內聯、編譯器選項、循環展開、條件執行、數據操作的轉化、存儲器的優化、代碼尺寸的優化等角度給出常用的優化方法。
  • 編寫C程序的7個步驟
    很多人覺得編寫一個C語言程序是個很複雜的問題,但其實是很簡單的,至少對於二級C考試題目來說都比較簡單。面對一個相對複雜的問題,我們要學會理清楚思路,把它分解成若干小問題,然後條理清晰地解決這個「複雜」的問題。
  • 用C語言編寫程序列印輸出九九乘法表
    用C語言編寫程序列印輸出九九乘法表C語言的功能十分強大,本人非常喜歡,九九乘法表既有C程序中的循環結構,也有輸出格式的設置,能夠體現出C程序的風格特點。下面就C程序編寫九九表分享給大家!int main()//定義整型主函數{int a,b,c;//定義整型變量a,b。for(a=1;a<=9;a++)//外層循環,從1到9,指的列印輸出9行。
  • 如何寫出高效優美的單片機C語言代碼?
    程序能跑起來並不見得你的代碼就是很好的c代碼了,衡量代碼的好壞應該從以下幾個方面來看1,代碼穩定,沒有隱患。本文引用地址:http://www.eepw.com.cn/article/201611/319838.htm2,執行效率高。3,可讀性高。4,便於移植。
  • 在 JS 中使用 C 程序
    即使沒有語言,人類也可以用機器指令來編寫程序。如果非要用 JavaScript 操作二進位,最終就類似這樣:var flags = +enableXX1雖然能實現,但很醜陋。各種硬編碼、各種位運算。emcc hello.c -o hello.html // hello.c#include #include  int main() {    time_t now;    time(&now);    printf("Hello
  • C程序編程四步走
    本文使用的測試代碼是經典入門程序 "Hello World!"。測試環境為探究預處理,編譯,彙編和連結的功能,我們在 Ubuntu 系統中使用 Gcc 編譯器( version=4.8.4 ),用簡單的也是最經典的入門程序 "Hello World!" 作為測試代碼。
  • 寫出高效優美的單片機C語言代碼
    程序能跑起來並不見得你的代碼就是很好的c代碼了,衡量代碼的好壞應該從以下幾個方面來看1,代碼穩定,沒有隱患。當然,在定義變量後不要超過變量的作用範圍,如果超過變量的範圍賦值,C編譯器並不報錯,但程序運行結果卻錯了,而且這樣的錯誤很難發現。在ICCAVR中,可以在Options中設定使用printf參數,儘量使用基本型參數(%c、%d、%x、%X、%u和%s格式說明符),少用長整型參數(%ld、%lu、%lx和%lX格式說明符),至於浮點型的參數(%f)則儘量不要使用,其它C編譯器也一樣。
  • 摘要:初學C語言的朋友,可能不會編寫大小寫字母轉換的代碼,現在就...
    初學C語言的朋友,可能不會編寫大小寫字母轉換的代碼,現在就由我分享給大家。希望對大家有所幫助。實現任意大小寫字母轉換。代碼如下:#include<stdio.h>void main(){ char x='a'; printf("請您輸入任意大寫字母或者小寫字母x:\n"); scanf("%c",&x); if( x>='A' && x<='Z') { x=x+32; } else if( x>='a' && x<='z') { x=x-32
  • python+C、C++混合編程的應用
    有的語言專注於簡單高效,比如python,內建的list,dict結構比c/c++易用太多,但同樣為了安全、易用,語言也犧牲了部分性能。在有些領域,比如通信,性能很關鍵,但並不意味這個領域的coder只能苦苦掙扎於c/c++的陷阱中,比如可以使用多種語言混合編程。
  • 基於TI C6000系列DSP的C/C++程序優化技術
    在此介紹TI C6000的軟體開發流程,重點討論C6000系列的C/C++程序優化技術,包括優化流程,C/C++代碼優化方法,編寫線形彙編代碼優化方法等。為DSP的C/C++軟體開發提供了全面的程序優化技術和方法,對實際系統的開發具有重要的現實意義。
  • Linux下C應用程式開發
    例如, 將一個叫 count.c 的 C 程序編譯為名叫 count 的可執行文件, 你將輸入下面的命令:  gcc -o count count.c--------------------------------------------------------------------------------
  • 用Python使用C語言程序(Windows平臺)
    本文的目標是在windows平臺下(使用pycharm),實現python調用C語言編寫的程序。主要參考資料:python擴展實現方法--python與c混和編程(http://www.cnblogs.com/btchenguang/archive/2012/09/04/2670849.html)混合編程:用 C 語言來擴展 Python 大法吧!
  • 【技術交流】編寫高效C語言的四大絕招
    編寫高效簡潔的C語言代碼,是許多軟體工程師追求的目標。本文就是針對編程工作中的一些體會和經驗做相關的闡述。宏函數僅僅作為預先寫好的代碼嵌入到當前程序,不會產生函數調用,所以僅僅是佔用了空間,在頻繁調用同一個宏函數的時候,該現象尤其突出。 第二招:數學方法解決問題 現在我們演繹高效C語言編寫的第二招--採用數學方法來解決問題。
  • 【C/C+】10個經典的C語言小程序,小白必看!
    程序原始碼: main() { int i,j,k; printf("\n"); for(i=1;i for(j=1;j
  • C語言,編寫程序將兩一維數組合併並排序
    編寫一個程序,將兩個元素從小到大有序的一維數組歸併成一個有序的一維數組。 要求:【輸入形式】用戶在第一行輸入第一個有序數組的元素數目,以回車結束此輸入。然後在第二行按照剛才輸入的元素數目依次輸入數組元素,中間用空格分隔,最後用回車結束輸入。