淺析代碼優(yōu)化
開篇
相信有過編碼經(jīng)驗的人都知道,程序的正常運行,只是最基本的要求。更多的,還要考慮程序的性能,運行效率,組織結(jié)構(gòu),和重用性等等。
今天將簡單的討論一下如何優(yōu)化程序性能。
要寫出高效的程序,可能多數(shù)初學(xué)者想到的是在程序中用合適的算法和數(shù)據(jù)結(jié)構(gòu)。這確實是一中提高程序性能的主要方法。
而這里要討論的是另一種方法,也是很多人都忽略但確實很重要的方法。也是我們這篇文章的主題:
如何編寫出編譯器能有效優(yōu)化的源代碼。
編譯器優(yōu)化的局限性
沒有萬能的東西,編譯器也一樣。現(xiàn)代編譯器都會對源代碼進行優(yōu)化,以提高程序的性能。比如linux下的GCC編譯器就能控制優(yōu)化的等級,優(yōu)化等級高,對應(yīng)的程序性能好。對于給定的代碼,編譯器并不能保證能得到最好的性能,它也有局限性。所以才需要程序員能寫出編譯器易于理解和優(yōu)化的代碼。
編譯器簡單的說是講程序語言代碼翻譯為機器碼,既然是翻譯,就不能改變程序想要表達的意思。所以編譯器對程序總是小心的使用安全的優(yōu)化。也就是說:優(yōu)化后的版本和未優(yōu)化的版本有一樣的行為。
下面簡單說一下編譯器的局限性
1、存儲器別名引用
存儲器別名引用就是兩個不同的指針可能指向存儲器中的同一個位置。
例子說明:看如下的代碼

兩個程序似乎有相同的行為。都是將存儲在*yp處的值兩次加到*xp處存儲的位置。這時,twiddle2的效率會更高一些。(可以認為第二個是第一個的簡單優(yōu)化)
第一個函數(shù)需要6次存儲器引用(讀*xp兩次,寫*xp兩次,讀*yp兩次)而第二個函數(shù)只需3次存儲器引用(讀*yp兩次,寫*xp一次)
當然上面討論的是在*xp和*yp指向不同位置的基礎(chǔ)上。
現(xiàn)在考慮*xp和*yp指向同一位置的情況:
函數(shù)twiddle1的結(jié)果是*xp的值翻四倍,twiddle2得到的是3倍。編譯器并不知道twiddle1會如何被引用,即不知道*xp,*yp是否指向存儲器的同一位置,如果指向同一位置,編譯器就不能把函數(shù)1優(yōu)化為函數(shù)2的形式。因為他們有不同的行為。
為了編譯后程序行為不被改變,也就是所謂的安全優(yōu)化,編譯器只能假設(shè)*xp 、*yp 會指向相同的位置。也就是說編譯器不會把函數(shù)1優(yōu)化為函數(shù)2的形式。這造成了一個妨礙優(yōu)化的因素。
2、函數(shù)副作用
用例子說明問題,看下面簡單的代碼:

簡單的看兩個函數(shù)能產(chǎn)生相同的結(jié)果。同樣的,可以暫時認為func2是func1的優(yōu)化版本。
但是func2 調(diào)用了f()一次,而func1調(diào)用了f()兩次。如果他們調(diào)用的函數(shù)f()修改了全局變量,結(jié)果就會有所不同
考慮如下f()代碼:
int f()
{
return counter++;
}
這個函數(shù)修改了全局變量counter,函數(shù)調(diào)用的次數(shù)會改變程序的行為。也就是說:這個函數(shù)有副作用。
大多數(shù)編譯器在代碼優(yōu)化的時候不會試圖判斷一個函數(shù)是否有副作用。為了安全的優(yōu)化,編譯器會認為所有函數(shù)都有副作用。
這也成為妨礙編譯器優(yōu)化的另一因素。
對于函數(shù)會有副作用的情況,在寫代碼的時候就要“幫助”編譯器做出判斷。和上面的代碼對應(yīng)的,如果函數(shù)f()沒有副作用,在寫程序的時候就把代碼寫成func2的形式。因為編譯器是不會把func1()優(yōu)化為func2()形式的。
消除循環(huán)的低效率
請看下面的代碼:

看for循環(huán)里,里面的判斷條件i<vec_length(v) (不用去管這個函數(shù)是干什么用的)我們知道,for循環(huán)每次都要判斷 i 的值是否滿足條件,按照上面的代碼也就意味著每次循環(huán)都要執(zhí)行函數(shù)vec_length(v)。如果這個函數(shù)的值不會因為循環(huán)而改變,那么把這個求值過程放在循環(huán)外面,只執(zhí)行一次函數(shù)就把值保存起來,會有更好的效果。
改進后的代碼如下:

這是一個常見的代碼優(yōu)化例子,稱為代碼移動。適用于要執(zhí)行多次(如在循環(huán)里)但計算結(jié)果不變的情況。而這類優(yōu)化是編譯器不能達到的。
編譯器會非常小心,為了安全,它認為所有調(diào)用的函數(shù)都有副作用。
消除不必要的存儲器引用
考慮下面的代碼:

看被圈起來的部分,OPER表示某種操作,for循環(huán)的目的是將數(shù)組data[]中的所有值依次執(zhí)行某種操作并把最后的值存入*dest。
觀察循環(huán)內(nèi)的代碼,下面列出每次循環(huán)對存儲器的操作:
1、讀*dest
2、讀data[i]
3、(通過計算)寫*dest
熟悉匯編的可以參看以上代碼的匯編形式:

對于第 i 次循環(huán),第 i 次讀的*dest的值剛好就是第 i-1 次寫入*dest的值。
由此可見,每次對于*dest的寫操作是多余的。因為下一次又會對他寫入覆蓋前面的值,而我們需要的只是最后一次寫入。
考慮到這里,可以用一個臨時變量來記錄*dest 的值,這樣就不用每次循環(huán)都重復(fù)對*dest的讀寫工作。在循環(huán)結(jié)束后講臨時變量的值寫入*dest即可。
如上所說,引入臨時變量x的代碼如下

對應(yīng)的匯編代碼:

小記
本文先討論了編譯器在代碼優(yōu)化方面的局限性,如簡單的存儲器別名引用和函數(shù)副作用。為了安全的優(yōu)化,編譯器總是考慮最糟的情況,他會認為所有的存儲器引用都會有別名引用,所有的函數(shù)都會有副作用。而這兩點成了限制編譯器優(yōu)化能力的很大因素。所以我們就有責任編寫出編譯器易于優(yōu)化的代碼。當確定了存儲器沒有別名引用時,或者當確定函數(shù)沒有副作用時,適當?shù)男薷拇a,能協(xié)助編譯器編寫出性能好的程序。
參考:《computer system》
http://en.wikipedia.org/wiki/Program_optimization
不足之處、歡迎拍磚
如轉(zhuǎn)載請著名出處:http://www.rzrgm.cn/yanlingyin/
一條魚 @博客園
2012-2-5

浙公網(wǎng)安備 33010602011771號