第五章 優化程式效能
寫程式的最主要目標就是使它在所有可能的情況下都正確工作。
程式設計師必須寫出“清晰簡潔”的程式碼,讀懂、理解、修改 程式碼。
編寫高效程式:
1. 選擇合適的演算法和資料結構
2. 編寫出編譯器能夠有效優化以轉換成高效可執行程式碼的原始碼。
3. 針對運算量特別大的計算,平行計算(將一個任務分成多個部分,這些部分可以在多核和多處理器的某種組合上並行地計算)。
程式優化:
l 第一步:消除不必要的內容,讓程式碼儘可能有效地執行它期望的工作。這包括不必要的函式呼叫、條件測試和儲存器引用。(5.4~5.6,不依賴於目標機器的任何特性)
l 第二步:利用處理器提供的指令級並行能力,同時執行多條指令。(5.7~5.9,利用處理器微體系結構 的優化)
程式碼剖析程式(profiler):
Amdahl定律:
研究程式的彙編程式碼表示,是理解編譯器,以及產生的程式碼如何執行的最有效的手段之一。
5.1、優化編譯器的能力和侷限性
儲存器別名使用(memory aliasing):兩個指標可能指向同一個儲存器位置的情況。
副作用:改變函式呼叫的次數會改變程式的行為。(修改了全域性程式狀態的一部分)。
妨礙優化的因素:
① 如果編譯器不能確定兩個指標是否指向同一個位置,就必須假設什麼情況都有可能,限制了可能的優化策略。
② 函式呼叫。大多數編譯器不會試圖判斷一個函式是否沒有副作用,因此任意函式都可能是優化的候選者。編譯器會假設最糟的情況,並保持所有的函式呼叫不變。
用行內函數替換優化函式呼叫:將函式呼叫替換為函式體,既減少了函式呼叫的開銷,也允許對展開的程式碼做進一步優化。
就優化能力來說,GCC被認為是勝任的,但是並不是特別突出,它完成了基本優化。
5.2、表示程式效能
每元素的週期數(CyclesPer Element,CPE):表示程式效能並指導我們改進程式碼的方法。
這樣的度量標準對執行重複計算的程式來說是很合適的。
處理器活動的順序是由時鐘控制的。用時鐘週期來表示,度量值表示的是執行了多少條指令。
5.3、程式示例
5.4、消除迴圈的低效率
程式碼移動(code motion):這類優化包括識別要執行多次(例如在迴圈裡)但是計算結果不會改變的計算。
程式設計師必須經常幫助編譯器顯式地完成程式碼移動。
庫函式strlen 的呼叫:
C語言中,字串是以null 結尾的字元序列,strlen必須一步一步的檢查這個序列,知道遇到null字元。對於一個長度為n的字串,strlen所用的時間與n成正比。
size_t strlen(const char *s)
{
size_t length =0 ;
while(*s != ‘\0’ )
{
s++;
length++;
}
returnlength;
}
一個看上去無足輕重的程式碼片段有隱藏的漸近低效率(asymptotic inefficiency)。對於一個100萬個字元的字串,這段無危險的程式碼變成了一個主要的效能瓶頸。
一個有經驗的程式設計師工作的一部分就是避免引入這樣的漸近低效率。
5.5、減少過程呼叫
過程呼叫會帶來相當大的開銷,而且妨礙大多數形式的程式優化。
消除迴圈中的函式呼叫,得到的程式碼執行速度快很多,這是以損害一些程式的模組性為代價的。
5.6、消除不必要的儲存器引用
在臨時變數中存放結果,消除了每次迴圈迭代中從儲存器中讀出並將更新值寫回的需要。
5.7、理解現代處理器
試圖進一步提高效能,必須考慮利用處理器微體系結構的優化,也就是處理器用來執行指令的底層系統設計。
在實際的處理器中,是同時對多條指令求值,這個現象稱為“指令級並行”。
多條指令可以並行地執行,同時又呈現一種簡單的順序執行指令的表象。
5.8、迴圈展開
迴圈展開:通過增加每次迭代計算的元素的數量,減少迴圈的迭代次數。
迴圈展開k次,上限設為(n-k+1),在迴圈內對元素i到i+j-1 應用合併運算,每次迭代,迴圈索引i加k。再處理後面的幾個元素。
迴圈展開對浮點運算沒有幫助,但是對整數加法和乘法有用。
5.9、提高並行性
(1)多個累積變數
將一組合並運算分割成兩個或更多的部分,並在最後合併結果來提高效能。
補碼運算時可交換和可結合的,甚至是當溢位時也是如此。整數運算是可結合的。
浮點乘法和加法不是可結合的,由於四捨五入和溢位,可能產生不同的結果。
(2)重新結合變換
括號改變合併順序。
迴圈展開和並行地累積在多個值中,是提高程式效能的更可靠的方法。
5.10、優化合並程式碼的結果小結
5.11、一些限制
(1)暫存器溢位
迴圈並行性的好處搜到描述計算的彙編程式碼的能力限制。如果並行度P超過了可用的暫存器數量,那麼編譯器會訴諸溢位(spolling),將某些臨時值存放到棧中。一旦出現這種情況,效能會急劇下降。
(2)分支預測和預測錯誤處罰
C程式設計師怎麼能夠保證分支預測處罰不會阻礙程式的效率呢?兩個通用原則:
Ø 不要過分關心可預測的分支
Ø 書寫適合用條件傳送實現的程式碼
5.12理解儲存器效能
所有的現代處理器都包含一個或多個快取記憶體儲存器(cache),以對這樣少量的儲存器提供快速的訪問。
如何編寫充分利用快取記憶體的程式碼,來提高程式效能。
(1) 載入
載入:從儲存器讀到暫存器
(2) 儲存
儲存:從暫存器寫到儲存器
5.13、應用:效能提高技術
優化程式效能的基本策略:
1) 高階設計:選擇適當的演算法和資料結構。
2) 基本編碼原則:避免限制優化的因素,編譯器就能產生高效的程式碼。
l 消除連續的函式呼叫。在可能時,將計算移到迴圈外。考慮有選擇地妥協程式的模組性以獲得更大的效率。
l 消除不必要的儲存器引用。引入臨時變數來儲存中間結果。只有在最後的值計算出來時,才將結果存放到陣列或全域性變數中。
3) 低階優化
l 展開迴圈。
l 用功能的風格重寫條件操作,使得編譯採用條件資料傳送。
5.14、確認和消除效能瓶頸
(1)程式剖析
程式剖析(profiling):包括執行程式的一個版本,其中插入了工具程式碼,以確定程式的各個部分需要多少時間。
程式碼剖析程式(codeprofiler)是在程式執行時收集效能資料的分析工具。
Unix系統提供一個剖析程式GPROF。產生兩種形式的資訊:
² 確定每個函式花費了多少CPU時間。
² 計算每個函式被呼叫的次數,以執行呼叫的函式來分類。
剖析報告:
第一部分是執行各個函式花費的時間,按照降序排列。
第二部分是函式的呼叫歷史。
GPROF屬性:
l 記時不是很準確。
l 呼叫資訊相當可靠
l 預設情況下,不會顯示對庫函式的呼叫。
(3) 用 剖析程式 來指導優化
(3)Amdahl 定律
主要思想:當我們加快系統一個部分的速度時,對系統整體效能的影響依賴於這個部分有多重要和速度提高了多少。
加速比:
Amdahl定律的主要觀點---要想大幅度提高整個系統的速度,我們必須提高整個系統很大一部分的速度。
5.15、小結