寫程序最主要的目標就是使它在全部可能的狀況下都正確工做。一個運行得很快可是給出錯誤結果的程序沒有任何用處。程序員必須寫出清晰簡潔的代碼,這樣作不只是爲了本身可以看懂代碼,也是爲了在檢査代碼和從此須要修改代碼時,其餘人可以讀懂和理解代碼。另外一方面,在不少狀況下,讓程序運行得快也是一個重要的考慮因素。本章主要介紹了循環展開,減少過程調用,消除沒必要要的內存引用等優化代碼的方法,有助於咱們寫出高效的代碼,提升代碼的性能。程序員
1.合適的算法和數據結構算法
2.編寫出編譯器可以有效優化以轉換成高效可執行代碼的源代碼(例如,在C語言中,指針運算和強制類型轉換使得編譯器很難對它進行優化)。數組
3.針對處理運算量特別大的計算,將一個任務分紅多部分,即利用並行性。緩存
-Og:默認配置,不優化。bash
-O1:編譯器試圖優化代碼大小和執行時間,它主要對代碼的分支,常量以及表達式等進行優化,但不執行任何會佔用大量編譯時間的優化。網絡
-O2:GCC執行幾乎全部不包含時間和空間權衡的優化(好比,嘗試更多的寄存器級的優化以及指令級的優化)。與-O相比,此選項增長了編譯時間,但提升了代碼的效率。數據結構
-O3:比-O2更優化,對於-O3編譯選項,在-O2的基礎上,打開了更多的優化項(好比,使用僞寄存器網絡,普通函數的內聯,以及針對循環的更多優化)。不過可能致使編譯出來的二級製程序不能debug。函數
-Os:主要是對代碼大小的優化,咱們基本不用作更多的關心。 一般各類優化都會打亂程序的結構,讓調試工做變得無從着手。而且會打亂執行順序,依賴內存操做順序的程序須要作相關處理才能確保程序的正確性。工具
兩個指針可能指向同一個內存位置的狀況成爲內存別名使用。oop
void twiddle1 (long *xp,long*yp) { *xp+ = *yp; *xp+ = *yp; } void twiddle2(long *xp,long*yp) { *xp+ = 2 * *yp; }twiddle1它們都是將存儲在由指針yp指示的位置處的值兩次加到指針xp指示的位置處的值。twiddle2須要3次內存引用(讀* xp,讀yp,寫* xp)。 twiddle1須要6次(2次讀* xp,2次讀yp,2次寫* xp),通常,咱們認爲twiddle2要優於twiddle2。那麼將twiddle1優化是否是就能產生相似twiddle2的代碼了?
答案顯然是不能夠的。當 *xp == *yp 時,twiddle1執行後的結果是:xp的值增長4倍。而twiddle2的結果是xp的值增長3倍。所以,兩個程序是有本質差異的。
以上這個例子就介紹了內存別名使用,編譯器在優化時,並不知道*xp 和 *yp是否相等,只能假設他們不相等,即xp和yp指針不會指向同一位置。
程序性能度量標準:每元素的週期數(Cycles Per Element,CPE)。
處理器活動的順序是由時鐘控制的,時鐘提供了某個頻率的規律信號,一般用千兆赫茲(GHz),即十億週期每秒來表示。例如,當代表一個系統有「4GHz」處理器,這表示處理器時鐘運行頻率爲每秒\(4 \times {10^9}\)個週期。每一個時鐘週期的時間是時鐘頻率的倒數。一般是以納秒( nanosecond,1納秒等於\({10^{ - 8}}\)秒)或皮秒( picosecond,1皮秒等於\({10^{ - 12}}\)秒)爲單位的。例如,一個4GHz的時鐘其週期爲0.25納秒,或者250皮秒。從程序員的角度來看,用時鐘週期來表示度量標準要比用納秒或皮秒來表示有幫助得多。用時鐘週期來表示,度量值表示的是執行了多少條指令,而不是時鐘運行得有多快。
舉個例子以下所示:
void lower1(char *s) { size_t i; for (i = 0; i < strlen(s); i++) if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); }
程序看起來沒什麼問題,一個很日常的大小寫轉換的代碼,可是爲何隨着字符串輸入長度的變長,代碼的執行時間會呈指數式增加呢?咱們把程序轉換成GOTO形式看下。
void lower1(char *s) { size_t i = 0; if (i >= strlen(s)) goto done; loop: if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); i++; if (i < strlen(s)) goto loop; done: }
咱們能夠看到,在C語言中調用strlen一次,這個函數實際上會執行兩次。還有一個重要的緣由是:字符串的長度並不會隨着循環的進行而改變,所以,咱們能夠把strlen放在循環外,避免每次都調用strlen進行計算。
由於C語言中的字符串是以null結尾的字符序列,strlen必須一步一步地檢查這個序列,直到遇到null字符。對於一個長度爲n的字符串,strlen所用的時間與n成正比。由於對lower1的n次迭代的每一次都會調用strlen,因此lower1的總體運行時間是字符串長度的二次項,正比於\({n^2}\)。
優化後的代碼以下所示:
void lower2(char *s) { size_t i; size_t len = strlen(s); /*放在函數體外*/ for (i = 0; i < len; i++) if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); }
兩者的執行效率比較以下所示:
這種優化是常見的一類優化的方法,稱爲代碼移動(Code motion)。這類優化包括識別要執行屢次(例如在循環裏)可是計算結果不會改變的計算。於是能夠將計算移動到代碼前面不會被屢次求值的部分。
/* Move call to vec_length out of loop */ void combine2 (vec_ptr v, data_t *dest) { long i; long length vec_length(v); *dest = IDENT; for (i=0;i< length;i++) { data_t val; get_vec_element(v,i,&val); *dest = *dest OP val; } }
從combine2的代碼中咱們能夠看出,每次循環迭代都會調用get_vec_element來獲取下一個向量元素。對每一個向量引用,這個函數要把向量索引i與循環邊界作比較,很明顯會形成低效率。在處理任意的數組訪問時,邊界檢查多是個頗有用的特性,可是對 combine2代碼的簡單分析代表全部的引用都是合法的。
data_t *get_vec_start(vec_ptr v) { return v-data; } /* Move call to vec_length out of loop */ void combine3 (vec_ptr v, data_t *dest) { long i; long length vec_length(v); data_t *data = get_vec_start(v); *dest = IDENT; for (i=0;i< length;i++) { *dest = *dest OP data[i]; } }
做爲替代,假設爲咱們的抽象數據類型增長一個函數get_vec_start。這個函數返回數組的起始地址,而後就能寫出此combine3所示的過程,其內循環裏沒有函數調用。它沒有用函數調用來獲取每一個向量元素,而是直接訪問數組。
事實上,通過這一步後,並無使得性能有較大提高,後面會詳細講到。這只是咱們優化路上的一步。
#Inner loop of combines. data_t double, OP = #dest in %rbx, data+i in %rdx, data+length in %rax .L17: vmovsd (%rbx),%xmm() # Read product from dest vmulsd (%rdx),%xmm0,%xmm0 # Multiply product by data[i] vmovsd %xmm, (%rbx) # Store product at dest addq $8,%rdx # Increment data+i cmp %rax,%rdx # Compare to data+length jne .L17
在這段循環代碼中,咱們看到,指針dest的地址存放在寄存器%rbx中,它還改變了代碼,將第i個數據元素的指針保存在寄存器%rdx中,註釋中顯示爲data+i。每次迭代,這個指針都加8。循環終止操做經過比較這個指針與保存在寄存器各ax中的數值來判斷。咱們能夠看到每次迭代時,累積變量的數值都要從內存讀出再寫入到內存。這樣的讀寫很浪費,由於每次迭代開始時從dest讀出的值就是上次迭代最後寫入的值。
咱們可以消除這種沒必要要的內存讀寫, combine4所示的方式以下。引入一個臨時變量acc,它在循環中用來累積計算出來的值。只有在循環完成以後結果才存放在dest中。正以下面的彙編代碼所示,編譯器如今能夠用寄存器%xmm0來保存累積值。
#Inner loop of combines. data_t double, OP = #dest in %rbx, data+i in %rdx, data+length in %rax .L25: vmulsd (%rdx),%xmm0,%xmm0 # Multiply product by data[i] addq $8,%rdx # Increment data+i cmp %rax,%rdx # Compare to data+length jne .L25
void combine4 (vec_ptr v, data_t *dest) { long i; long length vec_length(v); data_t *data = get_vec_start(v); *data acc = IDENT; for (i=0;i< length;i++) { acc = acc OP data[i]; } *dest = acc; }
把結果累積在臨時變量中。將累積值存放在局部變量acc(累積器( accumulator)的簡寫)中,消除了每次循環迭代中從內存中讀出並將更新值寫回的須要。
程序性能以下(以int整數爲例),單位爲CPE。
函數 | 方法 | + | * |
---|---|---|---|
combine3 | 直接數據訪問 | 7.17 | 9.02 |
combine4 | 累積在臨時變量中 | 1.27 | 3.01 |
超標量(superscalar):在每一個時鐘週期執行多個操做
亂序(out-of-order):指令執行的順序不必定要與它們在機器級程序中的順序一致
如上圖所示,爲一個亂序處理器的框圖。整個設計有兩個主要部分:指令控制單元( Instruction Control Unit,ICU)和執行單元( Execution Unit,EU)。前者負責從內存中讀出指令序列,並根據這些指令序列生成一組針對程序數據的基本操做;然後者執行這些操做。
指令高速緩存(Instruction cache):一個特殊的高速存儲器,它包含最近訪問的指令。
分支預測(branch prediction):處理器會猜想是否會選擇分支,同時還預測分支的目標地址。使用投機執行( speculative execution)的技術,處理器會開始取出位於它預測的分支,會跳到的地方的指令,並對指令譯碼,甚至在它肯定分支預測是否正確以前就開始執行這些操做。若是事後肯定分支預測錯誤,會將狀態從新設置到分支點的狀態,並開始取出和執行另外一個方向上的指令。標記爲取指控制的塊包括分支預測,以完成肯定取哪些指令的任務。
指令譯碼邏輯:一條指令會被譯碼成多個操做。例如,addq %rax,8(%rdx),。這條指令會被譯碼成爲三個操做:一個操做從內存中加載一個值處處理器中,一個操做將加載進來的值加上寄存器%rax中的值,而一個操做將結果存回到內存。
讀寫內存是由加載和存儲單元實現的。加載單元處理從內存讀數據處處理器的操做。這個單元有一個加法器來完成地址計算。相似,存儲單元處理從處理器寫數據到內存的操做。它也有一個加法器來完成地址計算。如圖中所示,加載和存儲單元經過數據高速緩存( data cache)來訪問內存。數據高速緩存是一個高速存儲器,存放着最近訪問的數據值。
退役單元( retirement unit):記錄正在進行的處理,並確保它遵照機器級程序的順序語義。退役單元控制這些寄存器的更新。指令譯碼時,關於指令的信息被放置在一個先進先出的隊列中。這個信息會一直保持在隊列中,直到發生如下兩個結果中的一個。首先,一旦一條指令的操做完成了,並且全部引發這條指令的分支點也都被確認爲預測正確,那麼這條指令就能夠退役( retired)了,全部對程序寄存器的更新均可以被實際執行了。另外一方面,若是引發該指令的某個分支點預測錯誤,這條指令會被清空( flushed),丟棄全部計算出來的結果。經過這種方法,預測錯誤就不會改變程序的狀態了。(任何對程序寄存器的更新都只會在指令退役時纔會發生)
寄存器重命名( register renaming):當一條更新寄存器r的指令譯碼時,產生標記t,獲得一個指向該操做結果的惟一的標識符。條目(r,t)被加入到一張表中,該表維護着每一個程序寄存器r與會更新該寄存器的操做的標記t之間的關聯。當隨後以寄存器r做爲操做數的指令譯碼時,發送到執行單元的操做會包含t做爲操做數源的值。當某個執行單元完成第一個操做時,會生成一個結果(v,t),指明標記爲t的操做產生值v。全部等待t做爲源的操做都能使用v做爲源值,這就是一種形式的數據轉發。經過這種機制,值能夠從一個操做直接轉發到另外一個操做,而不是寫到寄存器文件再讀出來,使得第二個操做可以在第一個操做完成後儘快開始。重命名錶只包含關於有未進行寫操做的寄存器條目。當一條被譯碼的指令須要寄存器r,而又沒有標記與這個寄存器相關聯,那麼能夠直接從寄存器文件中獲取這個操做數。有了寄存器重命名,即便只有在處理器肯定了分支結果以後才能更新寄存器,也能夠預測着執行操做的整個序列。
延遲( latency):它表示完成運算所須要的總時間。
發射時間( Issue time):它表示兩個連續的同類型的運算之間須要的最小時鐘週期數。(發射時間爲1:徹底流水線化)
浮點數加法器流水線化分爲三個階段:一個階段處理指數值,一個階段將小數相加,而另外一個階段對結果進行舍入。
容量( capacity):它表示可以執行該運算的功能單元的數量。
延遲界限:完成合並運算的函數所須要的最小CPE值。
最大吞吐量:發射時間的倒數。給出了CPE的最小界限。
循環展開是一種程序變換,經過增長每次迭代計算的元素的數量,減小循環的迭代次數。循環展開可以從兩個方面改進程序的性能。首先,它減小了不直接有助於程序結果的操做的數量,例如循環索引計算和條件分支。第二,它提供了一些方法,能夠進一步變化代碼,減小整個計算中關鍵路徑上的操做數量。
/*2 * 1 loop unrolling*/ /*使用2×1循環展開。這種變換能減少循環開銷的影響*/ void combine5(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc = (acc OP data[i]) OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }
上述代碼是使用的」2 * 1循環展開「的版本。第一個循環每次處理數組的兩個元素。也就是每次迭代,循環索引i加2,在一次迭代中,對數組元素i和i+1使用合併運算。(注意訪問不要越界,正確設置limit,n個元素,通常設置界限n-1)。
\(K \times 1\)循環展開次數和性能提高並非正比關係,通常來說,最多循環展開一次後,性能提高就不會很大了(主要緣由是關鍵路徑中n個mul操做,迭代次數減半,可是每次迭代中仍是有兩個順序的乘法操做。具體參考P367)。
\({P_n}\)表示\({a_0},{a_1} \cdots {a_n}\) 乘積:${P_n} = \sum\limits_{i = 0}^{n - 1} {{a_i}} \(。假設n爲偶數,咱們還能夠把它寫成\){P_n} = P{E_n} \times P{O_n}\(,這裏\)P{E_n}\(是索引爲偶數的元素的乘積,而\)P{O_n}$是索引值爲奇數的元素的乘積。
代碼以下:
/*2 * 2 loop unrolling*/ /*運用2×2循環展開。經過維護多個累積變量,這種方法利用了多個功能單元以及它們的流水線能力*/ void combine6(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc0 = IDENT; data_t acc1 = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc0 = acc0 OP data[i]; acc1 = acc1 OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc0 = acc0 OP data[i]; } *dest = acc0 OP acc1; }
上述代碼用了兩次循環展開,以使每次迭代合併更多的元素,也使用了兩路並行,將索引值爲偶數的元素累積在變量acc0中,而索引值爲奇數的元素累積在變量acc1中。所以,咱們將其稱爲」2×2循環展開」。同前面同樣,咱們還包括了第二個循環,對於向量長度不爲2的倍數時,這個循環要累積全部剩下的數組元素。而後,咱們對acc0和acc1應用合併運算,計算最終的結果。
事實上,combine6比combine5性能提高近2倍左右。
/*2 * 1a loop unrolling*/ /*運用2×1a循環展開,從新結合合併操做。這種方法增長了能夠並行執行的操做數量*/ void combine7(vec_ptr v, data_t *dest) { long i; long length = vec_length(v); long limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for (i = 0; i < limit; i+=2) { acc = acc OP (data[i] OP data[i+1]); } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }
咱們能夠看到關鍵路徑上只有n/2個操做。每次迭代內的第一個乘法都不須要等待前一次迭代的累積值就能夠執行。所以,最小可能的CPE減小了2倍。這種改進方式幾乎達到了吞吐量的極限。
在執行從新結合變換時,咱們又一次改變向量元素合併的順序。對於整數加法和乘法,這些運算是可結合的,這表示這種從新變換順序對結果沒有影響。對於浮點數狀況,必須再次評估這種從新結合是否有可能嚴重影響結果。咱們會說對大多數應用來講,這種差異不重要。
總的來講,從新結合變換可以減小計算中關鍵路徑上操做的數量,經過更好地利用功能單元的流水線能力獲得更好的性能。大多數編譯器不會嘗試對浮點運算作從新結合,由於這些運算不保證是可結合的。當前的GCC版本會對整數運算執行從新結合,但不是總有好的效果。一般,咱們發現循環展開和並行地累積在多個值中,是提升程序性能的更可靠的方法。
Intel在199年引入了SSE指令,SSE是「 Streaming SIMD Extensions(流SIMD擴展)」的縮寫,而SIMD(讀做「 sim-dee」)是「 Single-In-Struction, Multiple-Data(單指令多數據)」的縮寫。SSE功能歷經幾代,最新的版本爲高級向量擴展( advanced vector extension)或AVX。SIMD執行模型是用單條指令對整個向量數據進行操做。這些向量保存在一組特殊的向量寄存器( vector register)中,名字爲號%ymm0~%ymm15。目前的AVX向量寄存器長爲32字節,所以每個均可以存放8個32位數或4個64位數,這些數據既能夠是整數也能夠是浮點數。AVX指令能夠對這些寄存器執行向量操做,好比並行執行8組數值或4組數值的加法或乘法。例如,若是YMM寄存器%ymm0包含8個單精度浮點數,用\({a_0},{a_1} \cdots {a_n}\)表示,而%rcx包含8個單精度浮點數的內存地址,用\({b_0},{b_1} \cdots {b_n}\)表示,那麼指令vmulps (%rcx), %ymm0, %ymm1會從內存中讀出8個值,並行地執行8個乘法,計算\({a_i} \leftarrow {a_i}{b_i},0 \le i \le 7\),並將獲得的8個乘積保存到向量寄存器%ymm1。咱們看到,一條指令可以產生對多個數據值的計算,所以稱爲「SIMD」。(使用SIMD指令重寫代碼可使程序性能得到上百倍提高)
咱們能夠看到對這種循環展開程度的增長沒有改善CPE,有些甚至還變差了。現代x86-64處理器有16個寄存器,並可使用16個YMM寄存器來保存浮點數。一旦循環變量的數量超過了可用寄存器的數量,程序就必須在棧上分配一些變量。
例如,下面的代碼片斷展現了在10×10循環展開的內循環中,累積變量acc0是如何更新的:
# Updating of accumulator acco in 10 x 10 unrolling vmulsd (%rdx),%xmm,%xmm0 #acc0 *=data[i]
咱們看到該累積變量被保存在寄存器%xmm0中,所以程序能夠簡單地從內存中讀取data[i],並與這個寄存器相乘。
與之相比,20×20循環展開的相應部分很是不一樣:
# Updating of accumulator acco in 20 x 20 unrolling vmovsd 40(%rsp),%xmm0 vmulsd (%rdx),%xmm0,%xmm0 vmovsd %xmmO,40(%rsp)
累積變量保存爲棧上的一個局部變量,其位置距離棧指針偏移量爲40。程序必須從內存中讀取兩個數值:累積變量的值和data[i]的值,將二者相乘後,將結果保存回內存。
一旦編譯器必需要訴諸寄存器溢出,那麼維護多個累積變量的優點就極可能消失。幸運的是,x86-64有足夠多的寄存器,大多數循環在出現寄存器溢出以前就將達到吞吐量限制。
現代處理器的工做遠遠超前於當前正在執行的指令。
1.不要過度關心可預測的分支
咱們已經看到錯誤的分支預測的影響可能很是大,可是這並不意味着全部的程序分支都會減緩程序的執行。實際上,現代處理器中的分支預測邏輯很是善於辨別不一樣的分支指令的有規律的模式和長期的趨勢。例如,在合併函數中結束循環的分支一般會被預測爲選擇分支,所以只在最後一次會致使預測錯誤處罰。
2.書寫適合用條件傳送實現的代碼
程序中的許多測試是徹底不可預測的,依賴於數據的任意特性,例如一個數是負數仍是正數。對於這些測試,分支預測邏輯會處理得很糟糕。對於本質上沒法預測的狀況,若是編譯器可以產生使用條件數據傳送而不是使用條件控制轉移的代碼,能夠極大地提升程序的性能。
咱們發現GCC可以爲以一種更「功能性的」風格書寫的代碼產生條件傳送,在這種風格的代碼中,咱們用條件操做來計算值,而後用這些值來更新程序狀態,這種風格對立於一種更「命令式的」風格,這種風格中,咱們用條件語句來有選擇地更新程序狀態。
void minmax1(long a[],long b[],long n){ long i; for(i = 0;i,n;i++){ if(a[i]>b[i]){ long t = a[i]; a[i] = b[i]; b[i] = t; } } }
在隨機數據上測試這個函數,獲得的CPE大約爲13.50,而對於可預測的數據,CPE爲2.5其預測錯誤懲罰約爲20個週期。
用功能式的風格實現這個函數是計算每一個位置i的最大值和最小值,而後將這些值分別賦給a[i]和b[i]。
void minmax2(long a[],long b[],long n){ long i; for(i = 0;i,n;i++){ long min = a[i] < b[i] ? a[i]:b[i]; long max = a[i] < b[i] ? b[i]:a[i]; a[i] = min; b[i] = max; } }
對於這個函數的測試代表,不管數據是任意的,仍是可預測的,CPE都大約爲4.0。
現代處理器有專門的功能單元來執行加載和存儲操做,這些單元有內部的緩衝區來保存未完成的內存操做請求集合。一個包含加載操做的程序的性能既依賴於流水線的能力,也依賴於加載單元的延遲。
要肯定一臺機器上加載操做的延遲,咱們能夠創建由一系列加載操做組成的一個計算,一條加載操做的結果決定下一條操做的地址。舉例以下:
typedef struct ELE{ struct ELE *next; long data; }list_ele, *list_ptr; long list_len(list_ptr ls){ long len = 0; while (ls){ len++; ls = ls->next; } return len; }
.L3: #loop addq $1,%rax #Increment len moveq (%rdi),%rdi #ls = ls->next tesatq %rdi,%rdi #Test ls jne .L3 # if nonnull,goto loop
第3行上的movq指令是這個循環中關鍵的瓶頸。後面寄存器%rdi的每一個值都依賴於加載操做的結果,而加載操做又以%rdi中的值做爲它的地址。所以,直到前一次迭代的加載操做完成,下一次迭代的加載操做才能開始。這個函數的CPE等於4.00,是由加載操做的延遲決定的。
雖然只考慮了有限的一組應用程序,可是咱們能得出關於如何編寫高效代碼的很重要的經驗教訓。總結以下:
(1)高級設計
爲遇到的問題選擇適當的算法和數據結構。要特別警覺,避免使用那些會漸進地產生糟糕性能的算法或編碼技術。
(2)基本編碼原則
避免限制優化的因素,這樣編譯器就能產生高效的代碼。
消除連續的函數調用。在可能時,將計算移到循環外。考慮有選擇地妥協程序的模塊性以得到更大的效率。
消除沒必要要的內存引用。引入臨時變量來保存中間結果。只有在最後的值計算出來時,纔將結果存放到數組或全局變量中。
(3)低級優化
結構化代碼以利用硬件功能。
展開循環,下降開銷,而且使得進一步的優化成爲可能。
經過使用例如多個累積變量和從新結合等技術,找到方法提升指令級並行。
用功能性的風格重寫條件操做,使得編譯採用條件數據傳送。
(4)使用性能分析工具
當處理大型程序時,將注意力集中在最耗時的部分變得很重要。代碼剖析程序和相關的工具能幫助咱們系統地評價和改進程序性能。咱們描述了 GPROF,一個標準的Unix剖析工具。還有更加複雜完善的剖析程序可用,例如 Intel的VTUNE程序開發系統,還有 Linux系統基本上都有的 VALGRIND。這些工具能夠在過程級分解執行時間,估計程序每一個基本塊( basic block)的性能。(基本塊是內部沒有控制轉移的指令序列,所以基本塊老是整個被執行的。)
最後要給讀者一個忠告,要警戒,在爲了提升效率重寫程序時避免引入錯誤。在引人新變量、改變循環邊界和使得代碼總體上更復雜時,很容易犯錯誤。一項有用的技術是在優化函數時,用檢查代碼來測試函數的每一個版本,以確保在這個過程沒有引入錯誤。檢查代碼對函數的新版本實施一系列的測試,確保它們產生與原來同樣的結果。對於高度優化的代碼,這組測試狀況必須變得更加普遍,由於要考慮的狀況也更多。例如,使用循環展開的檢査代碼須要測試許多不一樣的循環界限,保證它可以處理最終單步迭代所須要的全部不一樣的可能的數字。
本章中詳細介紹了提升程序性能的一些通用方法和工具,在平常編寫代碼的過程當中,應時刻注意代碼的風格,養成良好的習慣。當處理大型程序時或者不知道優化程序的那個部分時,咱們能夠藉助GPROF,VTUNE,VALGRIND等工具來進一步剖析程序,分析每一個函數的性能,找到限制程序性能的因素,逐一解決。
有任何問題,都可經過公告中的二維碼聯繫我