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)查表(遊戲程序員必修課) 一個聰明的遊戲大蝦,基本上不會在自己的主循環裡搞什麼運算工作,絕對是先計算好瞭,再到循環裡查表。如果表很大,不好寫,就寫一個init函數,在循環外臨時生成表格。
(2)求餘運算
a=a%8; 可以改為: a=a&7;
說明:位操作隻需一個指令周期即可完成,而大部分的C編譯器的“%”運算均是調用子程序來完 成,代碼長、執行速度慢。通常,隻要求是求2n方的餘數,均可使用位操作的方法來代替。
(3)平方運算
a=pow(a, 2.0); 可以改為: a=a*a;
說明:在有內置硬件乘法器的單片機中(如51系列),乘法運算比求平方運算快得多,因為浮點數的求平方是通過調用子程序來實現的,在自帶硬件乘法器的AVR單片機中,如ATMega163中,乘法運算隻需2個時鐘周期就可以完成。既使是在沒有內置硬件乘法器的AVR單片機中,乘法運算的子程序比平方運算的子程序代碼短,執行速度快。
(4)用移位實現乘除法運算
a=a*4; b=b/4; 可以改為: a=a<<2; b=b>>2;
通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR中,如果乘以2n,都可以生成左移的代碼,而乘以其它的整數或除以任何數,均調用乘除法子程序。用移位的方法得到代碼比調用乘除法子程序生成的代碼效率高。實際上,隻要是乘以或除以一個整數,均可以用移位的方法得到結果。
(5)避免不必要的整數除法
整數除法是整數運算中最慢的,所以應該盡可能避免。一種可能減少整數除法的地方是連除,這裡除法可以由乘法代替。這個替換的副作用是有可能在算乘積時會溢出,所以隻能在一定范圍的除法中使用。
(6)使用增量和減量操作符
在使用到加一和減一操作時盡量使用增量和減量操作符,因為增量符語句比賦值語句更快,原因在於對大多數CPU來說,對內存字的增、減量操作不必明顯地使用取內存和寫內存的指令。顯然,不用取指令和存指令,增、減量操作執行的速度加快,同時長度也縮短瞭。
(7)使用復合賦值表達式 復合賦值表達式(如a-=1及a+=1等)都能夠生成高質量的程序代碼。
(8)提取公共的子表達式
在某些情況下,C++編譯器不能從浮點表達式中提出公共的子表達式,因為這意味著相當於對表達式重新排序。需要特別指出的是,編譯器在提取公共子表達式前不能按照代數的等價關系重新安排表達式。這時,程序員要手動地提出公共的子表達式。
4、結構體成員的佈局
很多編譯器有“使結構體字,雙字或四字對齊”的選項。但是,還是需要改善結構體成員的對齊,有些編譯器可能分配給結構體成員空間的順序與他們聲明的不同。但是,有些編譯器並不提供這些功能,或者效果不好。所以,要在付出最少代價的情況下實現最好的結構體和結構體成員對齊,建議采取下列方法:
(1)按數據類型的長度排序
把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。編譯器要求把長型數據類型存放在偶數地址邊界。在申明一個復雜的數據類型 (既有多字節數據又有單字節數據) 時,應該首先存放多字節數據,然後再存放單字節數據,這樣可以避免內存的空洞。編譯器自動地把結構的實例對齊在內存的偶數邊界。
(2)把結構體填充成最長類型長度的整倍數
把結構體填充成最長類型長度的整倍數。照這樣,如果結構體的第一個成員對齊瞭,所有整個結構體自然也就對齊瞭。
(3)按數據類型的長度排序本地變量
當編譯器分配給本地變量空間時,它們的順序和它們在源代碼中聲明的順序一樣,和上一條規則一樣,應該把長的變量放在短的變量前面。如果第一個變量對齊瞭,其它變量就會連續的存放,而且不用填充字節自然就會對齊。有些編譯器在分配變量時不會自動改變變量順序,有些編譯器不能產生4字節對齊的棧,所以4字節可能不對齊
(4)把頻繁使用的指針型參數拷貝到本地變量
避免在函數中頻繁使用指針型參數指向的值。因為編譯器不知道指針之間是否存在沖突,所以指針型參數往往不能被編譯器優化。這樣數據不能被存放在寄存器中,而且明顯地占用瞭內存帶寬。註意,很多編譯器有“假設不沖突”優化開關(在VC裡必須手動添加編譯器命令行/Oa或/Ow),這允許編譯器假設兩個不同的指針總是有不同的內容,這樣就不用把指針型參數保存到本地變量。否則,請在函數一開始把指針指向的數據保存到本地變量。如果需要的話,在函數結束前拷貝回去。
5、循環優化
(1)充分分解小的循環
要充分利用CPU的指令緩存,就要充分分解小的循環。特別是當循環體本身很小的時候,分解循環可以提高性能。註意:很多編譯器並不能自動分解循環。
(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循環
在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短於while循環。
(5)循環展開
這是經典的速度優化,但許多編譯程序(如gcc -funroll-loops)能自動完成這個事,所以現在你自己來優化這個顯得效果不明顯。
(6)循環嵌套
把相關循環放到一個循環裡,也會加快速度。
(7)Switch語句中根據發生頻率來進行case排序
Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,匹配時就跳轉到滿足條件的語句執行。所以可以對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高性能。此外,在case中推薦使用小的連續的整數,因為在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。
(8)將大的switch語句轉為嵌套switch語句
當switch語句中的case標號很多時,為瞭減少比較的次數,明智的做法是把大switch語句轉為嵌套switch語句。把發生頻率高的case 標號放在一個switch語句中,並且是嵌套switch語句的最外層,發生相對頻率相對低的case標號放在另一個switch語句中。
(9)循環轉置
有些機器對JNZ(為0轉移)有特別的指令處理,速度非常快,如果你的循環對方向不敏感,可以由大向小循環。不過千萬註意,如果指針操作使用瞭i值,這種方法可能引起指針越界的嚴重錯誤(i = MAX+1;)。當然你可以通過對i做加減運算來糾正,但是這樣就起不到加速的作用。
(10)公用代碼塊
一些公用處理模塊,為瞭滿足各種不同的調用需要,往往在內部采用瞭大量的if-then-else結構,這樣很不好,判斷語句如果太復雜,會消耗大量的時間的,應該盡量減少公用代碼塊的使用。(任何情況下,空間優化和時間優化都是對立的–東樓)。當然,如果僅僅是一個(3==x)之類的簡單判斷,適當使用一下,也還是允許的。記住,優化永遠是追求一種平衡,而不是走極端。
(11)提升循環的性能
要提升循環的性能,減少多餘的常量計算非常有用(比如,不隨循環變化的計算)。 如果已經知道if()的值,這樣可以避免重復計算。雖然不好的代碼中的分支可以簡單地預測,但是由於推薦的代碼在進入循環前分支已經確定,就可以減少對分支預測的依賴。
(12)選擇好的無限循環
在編程中,我們常常需要用到無限循環,常用的兩種方法是while (1) 和 for (;;)。這兩種方法效果完全一樣,但那一種更好呢?編譯後,for (;;)指令少,不占用寄存器,而且沒有判斷、跳轉,比while (1)好。
6、提高CPU的並行性
(1)使用並行代碼
盡可能把長的有依賴的代碼鏈分解成幾個可以在流水線執行單元中並行執行的沒有依賴的代碼鏈。很多高級語言,包括C++,並不對產生的浮點表達式重新排序,因為那是一個相當復雜的過程。需要註意的是,重排序的代碼和原來的代碼在代碼上一致並不等價於計算結果一致,因為浮點操作缺乏精確度。在一些情況下,這些優化可能導致意料之外的結果。幸運的是,在大部分情況下,最後結果可能隻有最不重要的位(即最低位)是錯誤的。
(2)避免沒有必要的讀寫依賴
當數據保存到內存時存在讀寫依賴,即數據必須在正確寫入後才能再次讀取。雖然AMD Athlon等CPU有加速讀寫依賴延遲的硬件,允許在要保存的數據被寫入內存前讀取出來,但是,如果避免瞭讀寫依賴並把數據保存在內部寄存器中,速度會更快。在一段很長的又互相依賴的代碼鏈中,避免讀寫依賴顯得尤其重要。如果讀寫依賴發生在操作數組時,許多編譯器不能自動優化代碼以避免讀寫依賴。所以推薦程序員手動去消除讀寫依賴,舉例來說,引進一個可以保存在寄存器中的臨時變量。這樣可以有很大的性能提升。
7、循環不變計算
對於一些不需要循環變量參加運算的計算任務可以把它們放到循環外面,現在許多編譯器還是能自己幹這件事,不過對於中間使用瞭變量的算式它們就不敢動瞭,所以很多情況下你還得自己幹。對於那些在循環中調用的函數,凡是沒必要執行多次的操作通通提出來,放到一個init函數裡,循環前調用。另外盡量減少喂食次數,沒必要的話盡量不給它傳參,需要循環變量的話讓它自己建立一個靜態循環變量自己累加,速度會快一點。 還有就是結構體訪問,東樓的經驗,凡是在循環裡對一個結構體的兩個以上的元素執行瞭訪問,就有必要建立中間變量瞭(結構這樣,那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結構,然後嵌套在一起,這樣可以避免無謂的判斷。
總結
到此這篇關於C++程序代碼優化的文章就介紹到這瞭,更多相關C++代碼優化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!