C\C++代碼優化的27個建議
funccost 是函數func運行時間百分比,funcspeedup 是你優化函數的運行的係數。
因此,若是你優化了函數TriangleIntersect 執行40%的運行時間,使它運行快了近兩倍,而你的程序會運行快25%。
這意味着不常用的代碼不須要作較多優化考慮(或者徹底不優化)。
這裏有句俗語:讓常常執行的路徑運行更加高效,而運行稀少的路徑正確運行。
2. 代碼先保證正確,而後再考慮優化
這並不意味着用8周時間寫一個全功能的射線追蹤算法,而後用8周時間去優化它。
分多步來作性能優化。
先寫正確的代碼,當你意識到這個函數可能會被常常調用,進行明顯的優化。
而後再尋找算法的瓶頸,並解決(經過優化或者改進算法)。一般,改進算法能顯著地改進瓶頸——也許是採用一個你尚未預想到的方法。全部頻繁調用的函數,都須要優化。
3. 我所瞭解的那些寫出很是高效代碼的人說,他們優化代碼的時間,是寫代碼時間的兩倍。
4.跳轉和分支執行代價高,若是可能,儘可能少用。
函數調用須要兩次跳轉,外加棧內存操做。
優先使用迭代而不是遞歸。
使用內聯函數處理短小的函數來消除函數調用開銷。
將循環內的函數調用移動到循環外(例如,將for(i=0;i <100;i++) DoSomething(); 改成DoSomething() { for(i=0;i <100;i++) { … }})。
if…else if…else if…else if…很長的分支鏈執行到最後的分支須要不少的跳轉。若是可能,將其轉換爲一個switch聲明語句,編譯器有時候會將其轉換爲一個表查詢單次跳轉。若是switch聲明不可行,將最多見的場景放在if分支鏈的最前面。
5. 仔細思考函數下標的順序。
兩階或更高階的數組在內存中仍是以一維的方式在存儲在內存中,這意味着(對於C/C++數組)array[i][j] 和 array[i][j+1] 是相鄰的,可是array[i][j] 和array[i+1][j] 可能相距很遠。
以適當的方式訪問存儲實際內存中的數據,能夠顯著地提高你代碼的執行效率(有時候能夠提高一個數量級甚至更多)。
現代處理器從主內存中加載數據處處理器cache,會加載比單個值更多的數據。該操做會獲取請求數據和相鄰數據(一個cache行大小)的整塊數據。這意味着,一旦array[i][j] 已經在處理器cache中, array[i][j+1] 很大可能也已經在cache中了,而array[i+1][j] 可能還在內存中。
6. 使用指令層的並行機制
儘管許多程序仍是依賴單線程的執行,現代處理器在單核中也提供了很多的並行性。例如:單個CPU能夠同時執行4個浮點數乘,等待4個內存請求並執行一個分支預判。
爲了最大化利用這種並行性,代碼塊(在跳轉之間的)須要足夠的獨立指令來容許處理器被充分利用。
考慮展開循環來改進這一點。
這也是使用內聯函數的一個好理由。
7. 避免或減小使用本地變量。
本地變量一般都存儲在棧上。不過若是數量比較少,它們能夠存儲在CPU寄存器中。在這種狀況下,函數不但獲得了更快訪問存儲在寄存器中的數據的好處,也避免了初始化一個棧幀的開銷。
不要將大量數據轉換爲全局變量。
8. 減小函數參數的個數。
和減小使用本地變量的理由同樣——它們也是存放在棧上。
9. 經過引用傳遞結構體而不是傳值
我在射線追蹤中還找不到一個場景須要將結構體使用傳值方式(包括一些簡單結構如:Vector,Point和Color)。
10. 若是你的函數不須要返回值,不要定義一個。
11. 儘可能避免數據轉換。
整數和浮點數指令一般操做不一樣的寄存器,因此轉換須要進行一次拷貝操做。
短整型(char和short)仍然使用一整個寄存器,而且它們須要被填充爲32/64位,而後在存儲回內存時須要再次轉換爲小字節(不過,這個開銷必定比一個更大的數據類型的內存開銷要多一點)。
12. 定義C++對象時須要注意。
使用類初始化而不是使用賦值(Color c(black); 比Color c; c = black; 更快)
13. 使類構造函數儘量輕量。
尤爲是經常使用的簡單類型(好比,color,vector,point等等),這些類常常被複制。
這些默認構造函數一般都是在隱式執行的,這或許不是你所指望的。
使用類初始化列表(Use Color::Color() : r(0), g(0), b(0) {},而不是初始化函數Color::Color() { r= g = b = 0; } .)
14. 若是能夠的話,使用位移操做>>和<<來代替整數乘除法
15. 當心使用表查找函數
許多人都鼓勵將複雜的函數(好比:三角函數)轉化爲使用預編譯的查找表。對於射線追蹤功能來講,這一般致使了沒必要要的內存查找,這很昂貴(並不斷增加),而且這和計算一個三角函數並從內存中獲取值同樣快(尤爲你考慮到三角查找打亂了cpu的cache存取)。
在其餘狀況下,查找表會頗有用。對於GPU編程一般優先使用表查找而不是複雜函數。
16. 對大多數類,優先使用+= 、 -= 、 *= 和 /=,而不是使用+ 、 -、 * 、 和?/
這些簡單操做須要建立一個匿名臨時中間變量。
例 如:Vector v = Vector(1,0,0) + Vector(0,1,0) + Vector(0,0,1);?建立了五個匿名臨時Vector: Vector(1,0,0), Vector(0,1,0), Vector(0,0,1), Vector(1,0,0) + Vector(0,1,0), 和 Vector(1,0,0) + Vector(0,1,0) + Vector(0,0,1).
對 上述代碼進行簡單轉換:Vector v(1,0,0); v+= Vector(0,1,0); v+= Vector(0,0,1);僅僅建立了兩個臨時Vector: Vector(0,1,0) 和 Vector(0,0,1)。這節約了6次函數調用(3次構造函數和3次析構函數)。
17. 對於基本數據類型,優先使用+?、? -?、? *?、? 和?/,而不是+=?、? -=?、? *= 和 /=
18. 推遲定義本地變量
定義一個對象變量一般須要調用一次函數(構造函數)。
若是一個變量只在某些狀況下須要(例如在一個if聲明語句內),僅在其須要的時候定義,這樣,構造函數僅在其被使用的時候調用。
19. 對於對象,使用前綴操做符(++obj),而不是後綴操做符(obj++)
這在你的射線追蹤算法中可能不是一個問題
使用後綴操做符須要執行一次對象拷貝(這也致使了額外的構造和析構函數調用),而前綴的構造函數不須要一個臨時的拷貝。
20. 當心使用模板
對不一樣的是實例實現進行不一樣的優化。
標準模板庫已經通過良好的優化,不過我建議你在實現一個交互式射線追蹤算法時避免使用它。
使用本身的實現,你知道它如何使用算法,因此你知道如何最有效的實現它。
最重要的是,個人經歷告訴我:調試STL庫很是低效。一般這也不是一個問題,除非你使用debug版本作性能分析。你會發現STL的構造函數,迭代器和其餘一些操做,佔用了你15%的運行時間,這會致使你分析性能輸出更加費勁。
21. 避免在計算時進行動態內存分配
動態內存對於存儲場景和運行期間其餘數據都頗有用。
可是,在許多(大多數)的系統動態內存分配須要獲取控制訪問分配器的鎖。對於多線程應用程序,現實中使用動態內存因爲額外的處理器致使了性能降低,由於須要等待分配器鎖和釋放內存。
即使對於單線程應用,在堆上分配內存也比在棧上分配內存開銷大得多。操做系統還須要執行一些操做來計算並找到適合尺寸的內存塊。
22. 找到你係統內存cache的信息並利用它們
若是一個是數據結構正好適合一個cache行,處理整個類從內存中只須要作一次獲取操做。
確保全部的數據結構都是cache行大小對齊(若是你的數據結構和一個cache行大小都是128字節,仍有可能由於你的結構體中的一個字節在一個cache行中,而其餘127字節在另一個cahce行中)。
23. 避免不須要的數據初始化
若是你須要初始化一大段的內存,考慮使用memset。
24. 儘早結束循環和儘早返回函數調用
考慮一個射線和三角形交叉,一般的狀況是射線會越過三角,因此這裏能夠優化。
若是你決定將射線和三角面板交叉。若是射線和麪板交叉t值是負數,你能夠當即返回。這容許你跳過射線三角交叉一大半的質心座標計算。這是一個大的節約,一旦你知道這個交叉不存在,你就應該當即返回交叉計算函數。
一樣的,一些循環也應該儘早結束。例如,當設置陰影射線,對於近處的交叉一般都是沒必要須的,一旦有相似的的交叉,交叉計算就應該儘早返回。(這裏的交叉含義不太明白,多是專業詞彙,譯者注)
25. 在稿紙上簡化你的方程式
許多方程式中,一般均可以或者在某些條件中取消計算。
編譯器不能發現這些簡化,可是你能夠。取消一個內部循環的一些昂貴操做能夠抵消你在其餘地方的好幾天的優化工做。
26. 整數、定點數、32位浮點數和64位雙精度數字的數學運算差別,沒有你想象的那麼大
在現代CPU,浮點數運算和整數運算差很少擁有一樣的效率。在計算密集型應用(好比射線追蹤),這意味這能夠忽略整數和浮點數計算的開銷差別。這也就是說,你沒必要要對算數進行整數處理優化。
雙 精度浮點數運算也不比單精度浮點數運算更慢,尤爲是在64位機器上。我在同一臺機器測試射線追蹤算法所有使用double比所有使用floats運行有時 候更快,反過來測試也看到了同樣的現象(這裏的原文是:I have seen ray tracers run faster using all doubles than all floats on the same machine. I have also seen the reverse)。
27. 不斷改進你的數學計算,以消除昂貴的操做
sqrt()常常能夠被優化掉,尤爲是在比較兩個值的平方根是否一致時。
若是你重複地須要處理 除x 操做,考慮計算1/x的值,乘以它。這在向量規範化(3次除法)運算中贏得了大的改進,不過我最近發現也有點難以肯定的。不過,這仍然有所改進,若是你要進行三次或更多除法運算。
若是你在執行一個循環,那些在循環中執行不發生變化的部分,確保提取到循環外部。
考慮看看你的計算值是否能夠在循環中修改獲得(而不每次都從新開始循環計算)。
歡迎關注本站公眾號,獲取更多信息