深刻理解計算機系統(5.1)------優化程序性能

  你能得到的對程序最大的加速比就是當你第一次讓它工做起來的時候。java

  在講解如何優化程序性能以前,咱們首先要明確寫程序最主要的目標就是使它在全部可能的狀況下都能正常工做,一個運行的很快的程序可是倒是錯誤的結果是沒有任何用處的,因此咱們在進行程序性能優化以前,首先要保證程序能正常運行,且結果是咱們須要的。程序員

  並且在不少狀況下,讓程序跑的更快是咱們必需要解決的問題。好比一個程序要實時處理視頻幀或者網絡包,那麼一個運行的很慢的程序就不能解決此問題。再好比一個計算任務計算量很是大,須要很多天或者數週,若是咱們哪怕只是讓它運行的快20%也會產生重大影響。算法

 

一、編寫高效程序的切入點

  ①、選擇一組合適的算法和數據結構。編程

  ②、編寫出編譯器可以有效優化以轉換成高效可執行的源代碼。緩存

  ③、多線程並行處理運算。安全

  對於第一點,程序=數據結構+算法,選擇合適的數據結構和算法無疑對於提升程序的運行效率有很大的影響。第二點對於編程者則須要理解編譯器的優化能力以及侷限性,編寫程序看上去只是一點小小的改動,可能都會引發編譯器優化方式很大的變化;第三點技術主要這對運算量特別大的運算,咱們將一個大的任務分紅多個小任務,這些任務又能夠在多核和多處理器的某種組合上並行的計算,這裏咱們也須要知道,即便是利用並行性,每一個並行的線程都要以最高性能的方式執行。性能優化

 

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

  正確性,正確性,正確性!!!這個要着重提醒,因此編譯器必須很當心的對程序使用安全的優化。限制編譯器只進行安全的優化,會消除一些形成錯誤的運行結果,可是這也意味着程序員必須花費更大的力氣寫出程序使編譯器可以將之轉換爲有效機器代碼。網絡

  對於下面兩個程序:數據結構

void add1(int *xp,int *yp){
     *xp += *yp;
     *xp += *yp;    
}

  

void add2(int *xp,int *yp){
     *xp += 2* *yp;
}

  對上上面兩個函數add1和add2,它們都是將存儲在由指針 yp 指示的位置處的值兩次加到指針 xp 指示的位置處的值。可是明顯add2的執行效率要高,它只要求 3 次存儲器的引用(讀*xp,讀*yp,寫*xp),而add1須要 6 次存儲器引用(2次讀*xp,2次讀*yp,2次寫*xp)。多線程

  下面有評論指出乘法指令要比加法指令慢不少,這裏的add1是兩次加法指令,而add2是一次乘法指令,按道理來說是add1要比add2快,但我這裏爲何說add2要快呢?咱們能夠看一下彙編級別的代碼:

  咱們經過執行 gcc -O0 -S add1.c 優化級別爲0,生成彙編代碼:

  

  同理經過執行 gcc -O0 -S add2.c 優化級別爲0,生成彙編代碼:

  

   很明顯,add2的乘法指令被轉換成一次加法指令了,雖然乘法指令確實比加法指令慢。可是要注意這裏是乘以2,若是倍數大於2,那就不必定了。

 

  所以,若是編譯器要優化add1,咱們能夠認爲add2是其優化後的代碼。但實際上真的是這樣嗎?若是 xp 等於 yp,那麼變成以下:

void add1(int *xp,int *xp){
     *xp += *xp;
     *xp += *xp;    
}

   

void add2(int *xp,int *xp){
     *xp += 2* *xp;
}

  咱們能夠看到,這個時候對於 add1,xp的值會增長 4 倍,可是 add2 當中,xp 的值只增長 3 倍。因爲編譯器不知道參數 xp 和 yp 是否相等,它必須假定他們有可能相等,因此不會產生 add2 做爲 add1 的優化版本。

  在各類編譯器中,咱們前面說過的 gcc 編譯器,能夠經過加參數O0 -->> O1 -->> O2 -->> O3,分別是從沒有優化到優化級別最高。可是基本上編譯器都不會對程序進行各類激進的優化,因此程序員必須以一種簡化編譯器生成高效代碼的任務來編寫程序。如何編寫,請接着往下面看。

 

三、程序的性能表示

  處理器活動的順序是由時鐘控制的,時鐘提供了某個頻率的規律信號,一般用千兆赫茲(GHz),即十億週期每秒來表示。例如,當代表一個系統有「4GHz」處理器,這表示處理器時鐘運行頻率爲 4*109 千兆赫茲。每一個時鐘週期的時間是時鐘頻率的倒數。一般用納秒(nanosecond,1 納秒等於10-9秒),或者皮秒(picosecond,1 皮秒等於10-12秒)來表示,一個 4GHz 的十週週期爲0.25納秒,或者說250皮秒。從程序員的角度來看,用時鐘週期來表示度量標準要比用納秒或者皮秒來表示有用的多。

  用時鐘週期來表示,度量值表示的是執行了多少條指令,而不是時鐘運行的有多快。

 

四、提升程序的性能方法

  這本書的做者講解如何優化程序性能主要從兩個方面入手,第一個是與機器無關,第二個是與機器相關。

  與機器無關:

  ①、消除循環的低效率:將每次循環中執行屢次但計算結果不改變的部分提出循環,這樣只需計算一次,而不用循環一次,計算一次。以此提升算法效率。

  ②、減小過程調用:也就是減小函數方法的調用,由於函數方法的調用會帶來至關大的開銷。可是這樣也會帶來一個缺點,就是破壞程序的模塊化,因此咱們須要權衡利弊。

  ③、消除沒必要要的存儲器引用:在循環中不停地對指針所指向的變量賦值的時候,咱們能夠用一箇中間變量代替指針,以增長速度。

  ④、選擇合適的算法和數據結構:爲遇到的問題選擇合適的算法和數據結構,避免使用產生糟糕性能的算法或編碼技術。

  與機器相關:

  ①、理解現代處理器

  在代碼級上,看上去彷佛是一次執行條指令,每條指令都從寄存器或存儲器中取值,執行一個操做後,並把結果存到一個寄存器或存儲器位置。可是實際上,在處理器中是同時對多條指令求值,稱爲指令級並行。現代微處理器了不得的成就就是它們採用複雜而奇異的微處理結構,多條指令能夠並行執行,同時又呈現出一種簡單的順序執行指令的表象。

  當一系列操做必須按照嚴格的順序執行時,就會遇到延遲界限,由於在下一條指令開始以前,這條指令必須結束。當代碼中的數據相關限制令處理器利用指令級並行的能力時,延遲界限可以限定程序性能。吞吐量界限刻畫了處理器功能單元的原始計算能力。這個界限是程序性能的終極限制。

  下圖是一個現代微處理器的簡化示意圖:

  

  指令控制單元(Instruction Control Unit,ICU)從指令高速緩存(instruction cache)中讀取指令,併產生一系列基本操做。指令高速緩存是一個特殊的高速緩存存儲器,它存儲最近訪問的指令。一般ICU會在當前正在執行的指令很早以前取指,這樣它纔有足夠的時間對指令譯碼,並把操做發給執行單元 EU(Execution Unit ,EU),而後由EU完成ICU產生的基本操做。

  ②、提升並行性

循環分割,利用功能單元的流水線化的能力提升代碼性能。對於一個可結合和可交換的合併操做來講,好比說整數加法和乘法,咱們能夠經過將一組合並操做分割成兩個或更多的部分,經過在最後合併結果來提升性能。

相關文章
相關標籤/搜索