C\C++程式碼優化的27個建議

edithfang發表於2014-05-24

1. 記住阿姆達爾定律

Ahmdal's rule

  • 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 次除法)運算中贏得了大的改進,不過我最近發現也有點難以確定的。不過,這仍然有所改進,如果你要進行三次或更多除法運算。
  • 如果你在執行一個迴圈,那些在迴圈中執行不發生變化的部分,確保提取到迴圈外部。
  • 考慮看看你的計算值是否可以在迴圈中修改得到(而不每次都重新開始迴圈計算)。

英文原文:Tips for Optimizing C/C++ Code

相關閱讀
評論(1)

相關文章