第五章 優化程式效能

2021-08-16 01:56:44 字數 3654 閱讀 8994

寫程式的最主要目標就是使它在所有可能的情況下都正確工作。

程式設計師必須寫出「清晰簡潔」的**,讀懂、理解、修改   **。

編寫高效程式:

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)

returnlength;

}乙個看上去無足輕重的**片段有隱藏的漸近低效率

(asymptotic inefficiency)。對於乙個100萬個字元的字串,這段無危險的**變成了乙個主要的效能瓶頸。

乙個有經驗的程式設計師工作的一部分就是避免引入這樣的漸近低效率。

5.5、減少過程呼叫

過程呼叫會帶來相當大的開銷,而且妨礙大多數形式的程式優化。

消除迴圈中的函式呼叫,得到的**執行速度快很多,這是以損害一些程式的模組性為代價的。

5.6、消除不必要的儲存器引用

在臨時變數中存放結果,消除了每次迴圈迭代中從儲存器中讀出並將更新值寫回的需要。(p355中例子)

5.7、理解現代處理器

試圖進一步提高效能,必須考慮利用處理器微體系結構的優化,也就是處理器用來執行指令的底層系統設計。

在實際的處理器中,是同時對多條指令求值,這個現象稱為「指令級並行」。

多條指令可以並行地執行,同時又呈現一種簡單的順序執行指令的表象。

5.8、迴圈展開

迴圈展開:通過增加每次迭代計算的元素的數量,減少迴圈的迭代次數。

迴圈展開k次,上限設為(n-k+1),在迴圈內對元素i到i+k-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  展開迴圈,降低開銷。

i 通過使用例如多個累積變數和重新結合等技術,找到方法提高指令級並行。

l  用功能的風格重寫條件操作,使得編譯採用條件資料傳送。

5.14、確認和消除效能瓶頸

(1)程式剖析

程式剖析(profiling):包括執行程式的乙個版本,其中插入了工具**,以確定程式的各個部分需要多少時間。

**剖析程式(codeprofiler)是在程式執行時收集效能資料的分析工具。

unix系統提供乙個剖析程式gprof。產生兩種形式的資訊:

²  確定每個函式花費了多少cpu時間。

²  計算每個函式被呼叫的次數,以執行呼叫的函式來分類。

剖析報告:

第一部分是執行各個函式花費的時間,按照降序排列。

第二部分是函式的呼叫歷史。

gprof屬性:

l  記時不是很準確。

l  呼叫資訊相當可靠

l  預設情況下,不會顯示對庫函式的呼叫。

(3)    用 剖析程式 來指導優化

(3)amdahl 定律

主要思想:當我們加快系統乙個部分的速度時,對系統整體效能的影響依賴於這個部分有多重要和速度提高了多少。

加速比:

amdahl定律的主要觀點---要想大幅度提高整個系統的速度,我們必須提高整個系統很大一部分的速度。

5.15、小結(重要)

csapp3e 第五章 優化程式效能

前言 高效程式 1.適當的演算法和資料結構。1.將源 編碼成能夠被編譯器優化成高效可執行 消除連續的函式呼叫,消除不必要的記憶體引用 3,將運算量特別大的計算任務分成多個部分,這些部分可在多核和多處理器的某種組合上平行計算 第12章 展開迴圈,多個累計變數,重新結合,條件轉移 5.1優化編譯器的能力...

優化程式效能

編寫高效程式需要兩個活動 第一,我們必須選擇一組最好的演算法和資料結構 第二,我們必須編寫出編譯器能夠有效優化以轉換成高效可執行 的源 這裡,我們主要講述後者。首先,我們討論一下為什麼要編寫高效程式。不難想象,如果本來要用 天執行完的程式,經過優化只需要 天就可執行完,這是一件多麼令人振奮的 事啊。...

優化程式效能

l 消除迴圈的低效率 n 對於迴圈中的過程呼叫盡量移出迴圈外,例如 nfor i 0 i strlen s i strlen 函式為線性增長 在字串長度很大時 很消耗系統資源 n 減少不必要的儲存器引用,將儲存器引用儲存在臨時變數中.l 處理器優化 即充分利用儲存器流水線操作的吞吐量 n 迴圈展開,...