《Tips for Optimizing C/C++ Code》譯文

來源:互聯網
上載者:User

標籤:

前不久在微博上看到一篇非常好的短文講怎樣對C/C++進行效能最佳化,儘管其面向的領域是圖形學中的光線跟蹤,可是還是具有普遍的意義,將其翻譯成中文,希望對大家寫高品質代碼有協助。

1.     牢記阿姆達爾定律:加速比==

  • 當中表示函數func執行時間佔總時間的百分比,表示對該函數加速獲得的加速比。
  • 比如,你最佳化一個函數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通過查詢一個跳轉表實現了單步跳轉。假設不能改為switch結構,要將最可能被訪問的推斷條件放在條件鏈的最前面。

5.     細緻考慮數組下標的順序。

  • 二維或者更高維的數組在記憶體中實際上還是一維形式的存在。這就說明(針對C/C++數組)元素array[i][j]和array[i][j+1]在記憶體中位置相鄰,可是元素array[i][j]和array[i+1][j]卻能夠相差隨意個位置。
  • 假設我們在訪問記憶體中數組的時候是以一種近似線性順序實現的,代碼一般會獲得非常明顯的加速(有時會達到一個數量級,甚至更高)!
  • 如今的CPU在從記憶體載入資料到緩衝時,它不會僅僅載入要訪問的一個資料,而是會將一塊記憶體(cache line)都載入到緩衝中。這意味著當元素array[i][j]在緩衝中時,array[i][j+1]也非常有可能在緩衝中,可是array[i+1][j]卻依然在記憶體中。

6.     考慮指令級並行。

  • 雖然眼下還有非常多程式是單線程啟動並執行,可是如今的CPU早就在單核上實現了並行。這就意味著在單核上可能會同一時候運行4個浮點乘法,等待4個記憶體請求和即將到來的分支操作。
  • 為了充分利用指令級並行,代碼塊(跳轉指令之間)須要有充足的獨立指令(指令之間沒有相關性:資料相關、控制相關和名字相關)。
  • 能夠考慮迴圈展開。
  • 考慮內嵌函式。

7.     避免使用或者降低局部變數的數量。

  • 局部變數通常放在棧空間中。可是假設局部變數的個數足夠少,它們能夠被放在寄存器中。在這樣的情況下,函數能夠獲得比記憶體快得多的訪問速度,同一時候也避免了棧空間的布局。
  • 可是,也不要為了降低局部變數的個數將它們全都變為全域變數。

8.     降低函數的參數。

  • 和降低局部變數的原因一致—它們都放在棧中。

9.     結構體依照引用而不是依照值傳遞。

  • 據我所知,光線跟蹤代碼中沒有什麼結構體須要按值傳遞(即使簡單的向量,點和顏色等結構)。

10.  假設函數不須要返回值,就不要定義返回值。

11.  儘可能地避免類型強轉。

  • 整型和浮點型指令操作通常在不同的寄存器中,所以類型強轉意味著資料的複製。
  • 短整型(char和short)仍然須要一個完整的寄存器來儲存。在存到記憶體中時,才再次轉換成位元組儲存(只是是否須要強轉還須要考慮空間耗費的因素,強轉節省空間的可是費時)。

12.  聲明C++物件變數的時候要各位細緻。

  • 使用對象的初始化而不是賦值操作(Color c(black);比Color c; c=black;要快)。

13.  將類的預設建構函式設計得盡量小。

  • 特別是那些非常小可是常常被使用的類(如color,vector,point等)。
  • 預設建構函式常常會在你想不到的時候調用。
  • 使用建構函式初始化列表(使用Color::Color():r(0),g(0),b(0){},而不是Color::Color(){r=g=b=0;})。

14.  儘可能使用移位操作>>和<<取代整數的乘除法。

15.  在使用查表函數時要謹慎(光線跟蹤專用)。

  • 非常多人都建議查表使用預先計算的值來取代複雜的函數(比如三角函數)。可是在光線跟蹤中,這通常沒有必要。記憶體尋找非常耗時,這會抵消又一次計算三角函數的時間(尤其是考慮到查表會破壞CPU的緩衝結構時)。
  • 在其它場合下,查表一般會非常有協助。在GPU編程中,查表替代複雜操作是同意的。

16.  對大多數類操作,使用+= ,-= , *= , and /=而不是+ , - , * , and /。

  • 簡單操作是用來建立未命名或者暫時中間對象的。
  • 比如:Vector v = Vector(1,0,0) +Vector(0,1,0) + Vector(0,0,1);建立了5個未命名、暫時向量: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(0,1,0) andVector(0,0,1)。這會節約6次函數調用(3次建構函式和3次解構函式)。

17.  對於基礎資料型別 (Elementary Data Type)採用+ ,- , *,/而不是+= , -= , *= , and /=。

18.  延遲局部變數的聲明。

  • 聲明一個類對象須要一次函數調用(對建構函式的調用)。
  • 假設一個變數僅僅在某個條件下使用,則僅僅在真正須要的地方聲明該變數。

19.  對於對象(基本變數已被最佳化),使用首碼操作(++obj)取代尾碼操作(obj++)。

  • 在光線跟蹤中這不是一個問題。
  • 尾碼操作會帶來一次對象的複製(這須要額外調用一次建構函式和解構函式),可是首碼操作不須要暫時的複製。

20.  使用模板的時候要小心。

  • 不同執行個體化的最佳化方法不同!
  • 標準模板類已經被最佳化得非常徹底,可是還是避免使用它。
  • 為什麼不用?這是由於假設我們自己實現某個演算法,我們就會知道演算法細節以及最佳化方式。
  • 更重要的是,我的經驗告訴我在debug模式下編譯包括STL的代碼一般會非常緩慢。這一般不是一個問題,除非你使用debug版本號碼進行效能分析。你將會發現STL構造器,迭代器等會佔用15%以上的執行時間,這將導致效能輸出結果令人迷惑。

21.  在計算的過程中避免動態記憶體分配。

  • 動態記憶體分配適合於儲存情境和計算過程中不變的資料。
  • 然而,在大多數系統中,動態記憶體分配須要利用鎖來控制分配器的訪問。對於多線程應用,添加額外的處理器也可能會使效能下降,由於使用動態記憶體分配會導致線程不停地等待記憶體的分配和釋放。
  • 即使是對單線程應用,在堆上分配記憶體也比在棧上分配記憶體更耗時。由於作業系統須要做一些計算來尋找合適大小的記憶體塊。

22.  尋找和利用能夠最佳化系統緩衝訪問的一切資訊。

  • 假設一個資料結構能夠存放於單個cache line,則單次記憶體訪問就能夠將整個類載入到緩衝中。
  • 確保你的資料結構對齊到cache line的邊界(假設你的資料結構和cacheline都是128位元組,可是結構體的第1個位元組在第一個cache line中,剩餘的127位元組在第二個cache line中,那麼效能還是會很差)。

23.  避免不必要的資料初始化。

  • 假設你須要初始化大塊記憶體,請使用memset()。

24.  嘗試迴圈的提前終止和函數的提前返回。

  • 考慮一條光線和一個三角形的交。合理的情況是光線通只是三角形。這就是須要最佳化的地方。
  • 假設你須要確定光線和三角平面的交,當光線-平面交的t值為負時你能夠離開返回。這使你避免了求交過程中大約一半的質心計算。

25.  在紙上簡化方程。

  • 在非常多方程中,事實上有些項在某些情況下能夠被約去。
  • 編譯器不能發現方程的簡化,可是你行。有時僅僅是消除內部迴圈的一些耗時操作就能夠抵上你在其它地方工作好幾天的成果。

26.  整數、浮點數在數學上的差異可能沒有想象中大。

  • 在現代CPU中,浮點運算具有和整數運算同樣的輸送量。像光線跟蹤這種計算密集型應用,浮點操作和整數操作的差異很小。所以,沒有必要想方設法將操作轉成整數操作。
  • 雙精確度浮點數操作可能比單精確度慢,特別是在64位機器上。可是也可能會反過來。

27.  考慮將運算運算式變形以降低耗時操作。

  • sqrt()操作應該盡量被避免,特別是通過平方操作也能夠獲得相同結果時。
  • 假設你須要頻繁的進行除以x的操作,能夠考慮首先計算,然後將結果乘以。這在向量歸一化(3次除法)中會帶來較大收益。可是近期我發現情況變得有點難以推斷。只是,假設除法次數超過3次時,這樣的最佳化方法還是會帶來較大的效能提升。
  • 假設你在運行一個迴圈,請把計算結果保持不變的運算提到迴圈外面完畢。
  • 考慮能否夠增量計算一個變數(而不是每次都從頭開始計算該變數)。


《Tips for Optimizing C/C++ Code》譯文

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.