深入理解計算機系統_第五章_優化程序性能

檢討

在公司就有同事給我指出過:“大段貼代碼的技術博客都是垃圾”,如今反覆的體味到這句話很對。技術博客確實應該保持篇幅適中,思路流暢簡潔,我最近在看自己寫的深入理解計算機系統系列文章,真是又長又臭,第三章和第四張我自己都看不下去,基本是在摘抄原文中的話,那人家幹嘛看我這博客,人家自己買本書不好的多?在我這裏轉一手,除了排版更垃圾沒其他改變了,真是讓人失望透頂了。接下來我寫博客,一定要多加自己的理解,多畫結構圖,幫助自己和別人理清脈絡。

章前導讀

寫程序一定要在保證運行正確的基礎上儘可能運行的快,並且代碼要清晰簡潔,這樣在日後需要修改代碼時,其他人能夠讀懂和理解代碼。
編寫高效程序需要做到三點:1.必須選擇一組適當的算法和數據結構;2.必須編寫出編譯器能夠有效優化以轉換成高效可執行代碼的源代碼(對於這一點,理解優化編譯器的能力和侷限性很重要);3.將一個任務分成多個部分,這些部分可以在多核和多處理器的某種組合上並行計算。對於優化程序的一個挑戰就是儘管做了大量的變化,但還是要儘量維護代碼的簡潔和可讀性。
程序優化的第一步就是消除不必要的工作,讓代碼儘可能有效地執行所期望的任務。包括不必要的函數調用、條件測試和內存引用。
爲了使程序性能最大化,程序員和編譯器都需要知道一個模型,用來指明目標機器如何處理指令,以及各個操作的時序特性(比如知道了時序信息,就能確定是用一條乘法還是用移位和加法的組合)。文中給出了一種圖形數據流表示法(基於Intel和AMD處理器),可以使處理器對指令的執行形象化,可以利用它預測程序的性能。
瞭解處理器運作後,可以進行程序優化的第二步,利用處理器提供的指令級並行能力,同時執行多條指令。比如降低一個程序不同部分的數據相關程度,增加並行度,這樣就可以同時執行這些部分了。
最後本章還介紹了代碼剖析程序,該程序是測量程序各個部分性能的工具。可以幫助找到代碼中低效率的地方,進而確定應該着重優化的部分。
研究程序的彙編代碼表示是理解編譯器以及產生的代碼會如何運行的最有效手段之一。我們常常通過確認關鍵路徑來確定執行一個循環所需要的時間。關鍵路徑就是在循環中形成的數據相關鏈,然後回過頭來修改源代碼。

優化編譯器的能力和侷限性

GCC可以通過“-o1”、“-o2”、“-o3”來控制優化級別,更大的數字代表更大量的優化,這樣可以更好的提高性能,但也會讓程序規模變大,也更難以用標準的調試工具進行調試。

如果一個程序中有可能出現內存別名使用的情況(內存別名使用是指兩個指針可能指向同一個內存位置),編譯器對其優化的能力就大大減少了,因爲這兩個指針如果指向同一個內存,其結果與指向不同內存的結果大不相同,這就是一個妨礙優化的因素。如果編譯器不能確定兩個指針是夠指向同一位置,就必須假設什麼情況都可能,這就限制了可能的優化策略。

第二個妨礙優化的因素是函數調用,比如func1返回了f()+f()+f()+f(),func2返回4*f(),看起來返回的結果應該是一樣,但func1調用了4次,func2只調用了1次,看起來func2的效率更高。
但如果f()中改變了某一個全局數據,例如f()的函數主體是 return counter++(這個counter被定義爲全局變量,初始化爲0),那調用func1之後,counter = 0 + 1 + 2 + 3 = 6,而調用func2後,counter = 0。
也就是說,如果被調用的函數有一個副作用——修改了全局程序狀態的一部分(這個例子中是一個全局變量),那改變調用它的次數就會改變程序的行爲。
編譯器不會去判斷一個函數有沒有上述的副作用,而是默認最糟糕的情況,保持所有的函數調用不變。
tips:有時編譯器會利用內聯函數替換對包含函數調用的代碼進行優化(內聯函數替換就是將函數主體替換到調用處),這樣就不存在上述的問題了,因爲函數主體被直接替換了過來,不過要對函數進行追蹤或者設置斷點,或者用代碼剖析的方式評估程序性能的話,就不應該使用內聯函數替換了。

表示程序性能

在這裏插入圖片描述
上述的兩個函數psum1和psum2最終的效果是一樣的,當把兩個函數需要的時間和n的取值範圍做一個最小二乘擬合後,發現前者近似於368 + 9n,後者近似於368 + 6n。式子中n的係數成爲每元素週期數(CPE)的有效值,所以我們在優化程序時,會集中精力於減小CPE。根據這種量度標準,psum2的CPE爲6,優於CPE爲9.0的pusm1。

程序示例

文中設計了一個程序,用來比較優化後與優化前的的CPE。程序如下:
在這裏插入圖片描述
其中當OP爲+時,IDENT初始化爲0;OP爲*時,IDENT初始化爲1,vec_lenget函數計算出數據結構中數據部分的長度,get_vec_element函數最終會把結構體v中的第i個數據地址賦值給val。整個函數通過宏定義,實現了對一個數據結構中所有數據的加和或乘積。
在這裏插入圖片描述
圖中分別針對整數加法、乘法和浮點數加法、乘法比較了優化前後的CPE。

消除循環的低效率

上面的combine1函數,在for循環中調用了vec_length函數,每循環一次都需要調用該函數一次,但是調用結果又不會隨着循環進行而改變,因此只需要計算一次長度,此後直接用這個長度就可以了。修改如下:
在這裏插入圖片描述
測試結果如下:
在這裏插入圖片描述這種優化稱爲代碼移動。這類優化包括識別要執行多次但計算結果不會改變的計算。代碼移動前,往往程序會在小數據集上測試,這時候看不出什麼端倪,可當數據集變成100萬後,這段代碼就完完全全變成了危險炸彈。所以一定要避免引入這樣的漸近低效率。

-2019/5/14

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章