優化C/C++代碼的小技巧


說明:

無意看到一篇小短文,猜測作者應該是一個圖形學領域的程序員或專家,介紹了在光線(射線)追蹤程序中是如何優化C/C++代碼的。倒也有一些參考意義,當然有的地方我並不贊同或者說我也不完全理解,原文在此,我的粗糙翻譯如下:

 

1. 牢記Ahmdal定律

                  

  • funccost表示是函數func的運行時間百分比,funcspeedup是你優化后函數的運行系數;
  • 所以,如果函數TriangleIntersect()占用40%的運行時間,而在你優化后使它運行快了兩倍,那么你的程序運行能夠快了25%;
  • 這意味着不經常使用的代碼不需要做過多優化(或者完全不優化),比如場景加載過程;
  • 也就是:讓頻繁調用的代碼運行得更加高效,而讓較少調用的代碼保持運行正確;

2. 先有正確的代碼,然后再做優化

  • 這並不是說先花8個周時間寫一個全功能的光線追蹤器,然后再花8個周去優化;
  • 而是在你的管線追蹤程序中的多個階段都進行優化;
  • 如果代碼是正確的,而你又知道哪些函數會被頻繁的調用,優化是很明顯的;
  • 然后找到瓶頸所在,並去除瓶頸(通過優化或者算法改進)。通常來說改進算法可以很顯著地優化瓶頸——甚至可能采用了一個你沒想到的算法。優化那些你所知道的將被頻繁調用的函數是一個很好的做法;

3. 那些我認識的能夠寫出非常高效的代碼的人說,他們花費在優化代碼上的時間是他們寫代碼時間的至少兩倍以上 

4. 跳轉/分支語句是昂貴的,不管何時盡可能的減少使用

  • 函數調用除了棧存儲操作外,還需要兩次跳轉;
  • 優先選擇迭代,而不是遞歸;
  • 如果是短函數,使用內聯來消除函數開銷;
  • 將循環放在函數內(例如將for(i=0;i<100;i++) DoSomething();改為在DoSomething()內做DoSomething());
  • 長長的if...else if...else if...else if...語句鏈需要大量的跳轉才能結束(除了在測試每個條件時)。如果可能,改為switch語句,有時編譯器可以有優化為在一個表中查找和單級跳轉。如果switch語句是不可能的,那把最經常走到的if語句放在語句鏈開頭;

5. 考慮數組索引的順序

  • 兩維或更多維的數組在內存中仍是按一維存儲的。這意思是array[i][j]和 array[i][j+1]是相鄰的(C/C++代碼),然而array[i][j]和array[i+1][j]卻可以相離的任意遠;
  • 訪問物理內存中的連續數據,可以顯著加快你的代碼(有時是一個數量級,甚至更多);
  • 現在CPU從主內存中加載數據到高速緩存時,它不僅僅是只加載單一數據,而是加載一塊數據,既包含了要請求的數據,也包含部分相鄰數據(一個cache行)。這意思是說如果array[i][j]在CPU緩存中,那么array[i][j+1]就很有可能也在緩存中了,然而array[i+1][j]可能仍在內存中;

6. 考慮指令級並行性(IPL)

  • 盡管很多程序仍是單線程執行,但現代的CPU已經能夠在單核上有顯著的並行性。這意味着單CPU也可能同時執行4個浮點數乘法、等待4個內存請求,並執行即將到來的分支比較操作
  • 為了充分利用這種並行性,代碼塊(比如在跳轉語句中)需要足夠的獨立指令來使CPU得到充分使用;
  • 可以考慮通過展開循環來改進;
  • 這也是使用內聯函數的一個很好的原因;

7. 避免或減少局部變量的使用

  • 局部變量通常是存儲在棧上。如果很少,可以存儲在寄存器中。在這種情況下,函數不僅得到了對存儲在寄存器上的數據的更快內存訪問的好處,也可以避免建立一個棧幀的開銷;
  • 但是,也不要把所有對象都全盤聲明為全局變量;

8. 減少函數參數的個數

  • 和減少局部變量的原因一樣——他們也是在棧上存儲的;

9. 結構體(包括類)傳參時使用傳引用而不是傳值

  • 在光線追蹤程序中,哪怕是簡單如vector、points、colors等結構,我也沒有見過使用值傳遞的代碼

10. 如果你不需要一個函數的返回值,那就不要返回

11. 盡可能避免使用轉型操作

  • 整數和浮點數的指令集通常在不同的寄存器上運算,因此轉型操作需要拷貝操作;
  • 短整形(char和short)仍然需要一個全尺寸的寄存器,而且在存儲回內存之前,它們需要對齊到32位或64位上,然后才轉換成更小尺寸類型;

12. 當定義C++對象時一定要小心

  • 使用初始化(Color c(black))而不是賦值(Color c, c = black),而前者更快;

13. 使類的默認構造函數盡可能的輕量

  • 特別是那簡單的、經常使用的類(例如,顏色,矢量,點等);
  • 這些默認構造函數通常是在你不注意時就調用,甚至那時你並不希望這樣;
  • 使用構造初始化列表(使用Color::Color() : r(0), g(0), b(0) {}而不是Color::Color() { r = g = b = 0; } );

14. 盡可能使用移位操作符>>和<<,而不是整數乘法和除法

15. 小心使用查表功能

  • 很多人鼓勵對於復雜的功能(例如,三角函數)使用預先計算過值的查表法。對於光線跟蹤程序來說,這往往是不必要的。內存查找是非常(日益)昂貴的,而且重新計算三角函數往往和從內存中查找值一樣快(尤其是當你考慮到內存查找會影響CPU緩存命中率時);
  • 在其它情況下,查表可能是非常有用的。比如在GPU編程中,查表法通常是復雜功能的優先選擇;

16. 對於大多數的類類型,使用運算符 +=,-=,*=和/=,而少用+,-,*,/

  • 這類簡單操作其實需要創建一個匿名名的、臨時的中間對象;
  • 例如Vector v = Vector(1,0,0) + Vector(0,1,0) + Vector(0,0,1) 語句創建了5個未命名、臨時的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); 這樣僅僅創建了2個臨時Vector:Vector v(1,0,0) 和Vector(0,0,1),而節省了6個函數調用(3個構造和3個析構);

17. 對於基本數據類型,使用運算符+,-,*,/,而少用+=,-=,*=和/=

18. 延遲局部變量的定義時間

  • 定義一個對象總會有一個函數調用開銷(就是構造函數)
  • 如果一個對象只是有時候才被使用(比如在一個if語句內部),那么就只在必要時才定義,因為這樣就只當這個變量使用時才會調用它的構造函數

19. 對於對象來說,使用前綴操作符(++obj),而不是后綴操作符(obj++)

  • 在你的光線追蹤程序中,這可能並不是個問題
  • 對象的拷貝操作必須使用后綴操作符(這需要額外調用一個構造和一個析構函數),而前綴操作符並不產生臨時對象

20. 慎用模板

  • 各種具現化實例的優化方式可能是不同的;
  • 標准模板庫(STL)做了很好的優化,但如果你打算實現交互式光線跟蹤器,最好是仍避免使用;
  • 通過自己實現,你能清楚地明白要它使用的算法,你就會知道最有效的使用方式;
  • 更重要的是,我的經驗表明調試、編譯STL會很慢。通常這也是沒問題的,除非你使用Debug版本進行性能分析。你會發現STL的構造、迭代器等操作會占用運行時間的15%以上,它會使輸出的分析結果更為混亂

21. 在計算過程中避免動態內存分配

  • 動態內存主要優勢在於存儲場景數據和其他數據,而不是在計算過程中進行修改
  • 然而,在許多(大多數)時候系統動態存儲分配要求使用鎖來控制訪問分配器。對於使用動態內存的多線程應用程序來說,由於需要等待分配和釋放內存,通過這些額外的處理,你可能實際上得到的是一個更慢的程序
  • 即使在單線程程序中,在堆上分配內存也比在棧上分配更昂貴。操作系統需要進行一些計算來確定所需大小的內存塊。

22. 發現和充分利用有關你的系統內存Cache的有用信息

  • ü  如果一個數據結構大小恰好填滿一個Cache行,處理整個類只需要從內存中讀取一次;
  • ü  確保所有的數據結構都能對齊到Cache邊界(如果你的數據大小和Cache都是128字節,那么當1個字節在一個Cache行而另外127字節在第二個Cache行時,那么性能仍然不好)

23. 避免不必要的數據初始化

  • 如果你要初始化一大塊內存,考慮用memset()函數

24. 盡量提早結束循環判斷和函數返回

  • 考慮射線和三角形相交。常見情況是射線和三角形不相交,因此這里可以優化;
  • 如果你要判斷射線和三角形相交的情況,一旦t值射線平面為負,你可以立即返回。這樣可以使你跳過大約一半的光線三角形交叉點的重心坐標計算。一個巨大的勝利!一旦你確定沒有相交發生,求交函數就應該退出
  • 同樣的,一些循環也可以被提早結束。例如,在光線陰影設置中,最近的相交是不必要的。只要發現了任何交叉閉環,求交函數就可以返回

25. 先在紙上簡化你使用的公式

  • 在很多公式中,總是可以或者一些特殊情況下,可以取消計算
  • 編譯器找不到這些簡化,但是你可以。消除一些內在循環中的昂貴操作可以比你在其他地方的優化更能加速你的程序

26. 對於整數型、定點數、32位浮點數、64位浮點數來說,他們之間的差別並沒有你想象中的那么大

  • 現代CPU進行浮點運算和整數運算其實有相同的運算吞吐量,像光線追蹤這種計算密集型的程序,這意思是整數和浮點運算成本之間的差異可以忽略不計,這意味着你不需要做一些優化來使用整數運算;
  • 雙精度浮點運算並不一定比單精度浮點計算更慢,尤其是在64位機器上。我曾經在同一台機器上測試光線追蹤算法,結果是有時全部使用double比全部使用float會運行得更快,

27. 考慮通過重寫你的數學公式來消除昂貴的操作

  • sqrt()函數通常是可以避免的,尤其是在比較數值的平方是否相等時;
  • 如果你需要反復除以x,考慮計算1/x,然后相乘。在向量的歸一化操作中(3次除法),這曾經是一個很大的優化,但最近我發現這很難說。然而如果你整除更多次數,這樣做仍是有益的;
  • 如果你執行一個循環操作,將那些在循環中固定不變的計算移出到循環外;
  • 考慮是否能夠在計算循環自增中得到值(而不是每次迭代都計算),原文:Consider if you can compute values in a loop incrementally (instead of computing from scratch each iteration).
  • 上句改為:考慮是否可在循環中增量的計算結果,而不是在每次迭代時都重新計算(比如斐波那契數列)。該句由評論中的網友@clover_toeic所翻譯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM