1、基本優化
(1)全局變量
全局變量絕不會位於寄存器中。使用指針或者函數調用,可以直接修改全局變量的值。因此,
編譯器不能將全局變量的值緩存在寄存器中,但這在使用全局變量時便需要額外的(常常是不必要的)
讀取和存儲。所以,在重要的循環中我們不建議使用全局變量。
如果函數過多的使用全局變量,比較好的做法是拷貝全局變量的值到局部變量,
這樣它才可以存放在寄存器。這種方法僅僅適用於全局變量不會被我們調用的任意函數使用。
(2)用switch()函數替代if…else…
(3)使用二分方式中斷代碼而不是讓代碼堆成一列
(4)帶參數的宏定義效率比函數高。簡單的運算可以用宏定義來完成。
(5)選擇合適的算法和數據結構
選擇一種合適的數據結構很重要,如果在一堆隨機存放的數中使用了大量的插入和刪除指令,
那使用鏈表要快得多。數組與指針語句具有十分密切的關系,一般來說,指針比較靈活簡潔,
而數組則比較直觀,容易理解。對於大部分的編譯器,使用指針比使用數組生成的代碼更短,
執行效率更高。在許多種情況下,可以用指針運算代替數組索引,這樣做常常能產生又快又短的代碼。
與數組索引相比,指針一般能使代碼速度更快,占用空間更少。
(6)能使用指針操作的盡量使用指針操作,一般來說,指針比較靈活簡潔,對於大部分的編譯器,
使用指針生成的代碼更短,執行效率更高。
(7)遞歸調用盡量換成內循環或者查表解決,因為頻繁的函數調用也是很浪費資源的
查表是數據結構中的一個概念。查表的前提是先建表。
在C語言實現中,建表也就是將一系列的數據,或者有原始數據中提取出的特征值,
存儲到一定的數據結構中,如數組或鏈表中。
查表的時候,就是對數組或鏈表查詢的過程。常用的方式有如下幾種:
a、對於有序數組,可以采用折半查找的方式快速查詢。
b、 對於鏈表,可以根據鏈表的構建方式,進行針對性查詢算法的編寫。
c、大多數情況,可以通過遍歷的方式進行查表。即從第一個元素開始,一直順序查詢到最后一個元素,逐一對比。
(8)使用增量或減量操作符
++x;原因是增量符語句比賦值語句更快。
(9)使用復合賦值表達式
x+=1;
能夠生成高質量的程序代碼
(10)代碼中使用代碼塊可以及時回收不再使用的變量,提高性能。
變量的作用域從定義變量的那一行代碼開始,一直到所在代碼塊結束。
(11)當一個函數被調用很多次,而且函數中某個變量值是不變的,
應該將此變量聲明為static(只會分配一次內存),可以提高程序效率。
(12)循環:長循環在內,短循環在外。
2、用移位實現乘除法求余運算,避免不必要的整數除法
實際上,只要是乘以或除以一個整數,均可以用移位的方法得到結果,如: 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); }
避免不必要的整數除法 整數除法是整數運算中最慢的,所以應該盡可能避免。一種可能減少整數除法的地方是連除,這里除法可以由乘法代替。
這個替換的副作用是有可能在算乘積時會溢出,所以只能在一定范圍的除法中使用。 不好的代碼: int i, j, k, m; m = i / j / k; 推薦的代碼: int i, j, k, m; m = i / (j * k);
3、結構體成員的布局
(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; }
4、循環優化
(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)、Switch語句中根據發生頻率來進行case排序
Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。
當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,
匹配時就跳轉到滿足條件的語句執行。所以可以對case的值依照發生的可能性進行排序,
把最有可能的放在第一位,這樣可以提高性能。此外,在case中推薦使用小的連續的整數,
因為在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。
(6)、將大的switch語句轉為嵌套switch語句
當switch語句中的case標號很多時,為了減少比較的次數,
明智的做法是把大switch語句轉為嵌套switch語句。把發生頻率高的case 標號放在一個switch語句中,
並且是嵌套switch語句的最外層,發生相對頻率相對低的case標號放在另一個switch語句中。
比如,下面的程序段把相對發生頻率低的情況放在缺省的case標號內。
(7)、循環轉置
有些機器對JNZ(為0轉移)有特別的指令處理,速度非常快,
如果你的循環對方向不敏感,可以由大向小循環。
(8)、公用代碼塊
一些公用處理模塊,為了滿足各種不同的調用需要,往往在內部采用了大量的if-then-else結構,
這樣很不好,判斷語句如果太復雜,會消耗大量的時間的,應該盡量減少公用代碼塊的使用。
(任何情況下,空間優化和時間優化都是對立的--東樓)。當然,如果僅僅是一個(3==x)之類的簡單判斷,
適當使用一下,也還是允許的。記住,優化永遠是追求一種平衡,而不是走極端。
(9)提升循環的性能
要提升循環的性能,減少多余的常量計算非常有用(比如,不隨循環變化的計算)。
不好的代碼(在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()的值,這樣可以避免重復計算。雖然不好的代碼中的分支可以簡單地預測,
但是由於推薦的代碼在進入循環前分支已經確定,就可以減少對分支預測的依賴。
(10)、選擇好的無限循環
在編程中,我們常常需要用到無限循環,常用的兩種方法是while (1) 和 for (;;)。
這兩種方法效果完全一樣,但那一種更好呢?然我們看看它們編譯后的代碼:
編譯前: while (1); 編譯后: mov eax,1 test eax,eax je foo+23h jmp foo+18h 編譯前: for (;;); 編譯后: jmp foo+23h
顯然,for (;;)指令少,不占用寄存器,而且沒有判斷、跳轉,比while (1)好。
5、提高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; }
6、循環不變計算
對於一些不需要循環變量參加運算的計算任務可以把它們放到循環外面,
現在許多編譯器還是能自己干這件事,不過對於中間使用了變量的算式它們就不敢動了,
所以很多情況下你還得自己干。對於那些在循環中調用的函數,凡是沒必要執行多次的操作通通提出來,
放到一個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一次,然后將結果代入第二個式子,
節約了一次除法運算。
7、函數優化
(1)Inline函數
在C++中,關鍵字Inline可以被加入到任何函數的聲明中。這個關鍵字請求編譯器用函數內部的代碼替換
所有對於指出的函數的調用。這樣做在兩個方面快於函數調用:
第一,省去了調用指令需要的執行時間;第二,省去了傳遞變元和傳遞過程需要的時間。
但是使用這種方法在優化程序速度的同時,程序長度變大了,因此需要更多的ROM。
使用這種優化在Inline函數頻繁調用並且只包含幾行代碼的時候是最有效的。
(2)不定義不使用的返回值
函數定義並不知道函數返回值是否被使用,假如返回值從來不會被用到,
應該使用void來明確聲明函數不返回任何值。
(3)減少函數調用參數
使用全局變量比函數傳遞參數更加有效率。這樣做去除了函數調用參數入棧和函數完成后參數出棧所
需要的時間。然而決定使用全局變量會影響程序的模塊化和重入,故要慎重使用。
(4)所有函數都應該有原型定義
一般來說,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於優化的信息。
int max(int *a, int m, int n);//這行就是函數原型,函數定義在主函數后面。
//函數原型的就是實現函數先(main中調用),
//后(定義在后面)
(5)盡可能使用常量(const)
盡可能使用常量(const)。C++ 標准規定,如果一個const聲明的對象的地址不被獲取,
允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以生成更好的代碼。
(6)把本地函數聲明為靜態的(static)
如果一個函數只在實現它的文件中被使用,把它聲明為靜態的(static)以強制使用內部連接。
否則,默認的情況下會把函數定義為外部連接。這樣可能會影響某些編譯器的優化——比如,自動內聯。
8、變量
(1)register變量
在聲明局部變量的時候可以使用register關鍵字。這就使得編譯器把變量放入一個多用途的寄存器中,
而不是在堆棧中,合理使用這種方法可以提高執行速度。函數調用越是頻繁,越是可能提高代碼的速度。
(2)同時聲明多個變量優於單獨聲明變量
(3)短變量名優於長變量名,應盡量使變量名短一點
(4)在循環開始前聲明變量
(5)如果確定整數非負,應直接使用unsigned int,處理器處理無符號unsigned 整形數的效率遠遠高於有符號signed整形數
(6)局部變量盡可能的不使用char和short類型。
對於char和short類型,編譯器需要在每次賦值的時候將局部變量減少到8或者16位,
是通過寄存器左移24或者16位,然后根據有無符號標志右移相同的位數實現,
這會消耗兩次計算機指令操作
(7)使用盡量小的數據類型
能夠使用字符型(char)定義的變量,就不要使用整型(int)變量來定義;
能夠使用整型變量定義的變量就不要用長整型(long int),能不使用浮點型(float)變量就不要使用浮點型變量。
9、條件判斷
(1)使用switch替代if else
switch…case會生成一份大小(表項數)為最大case常量+1的跳表,程序首先判斷switch變量是否大於最大case 常量,若大於,則跳到default分支處理;否則取得索引號為switch變量大小的跳表項的地址(即跳表的起始地址+表項大小*索引號),程序接着跳到此地址執行
(2)在if(xxx1>XXX1 && xxx2=XXX2)多個條件判斷中,確保AND表達式的第一部分最快或最早得到結果,這樣第二部分便有可能不需要執行
(3)在必須使用if…else…語句,將最可能執行的放在最前面
(4)使用嵌套的if結構
在if結構中如果要判斷的並列條件較多,最好將它們拆分成多個if結構,然后嵌套在一起,這樣可以避免無謂的判斷。
參考引用:
C 應用程序性能優化方法
https://jingyan.baidu.com/article/8ebacdf0730c0f49f65cd500.html
C語言編程優化運行速度
https://blog.csdn.net/sunjiajiang/article/details/7887724?utm_medium=distribute.pc_relevant.none-task-blog-title-2&spm=1001.2101.3001.4242
C語言高效編程與代碼優化
https://segmentfault.com/a/1190000037447486