開篇
相信有過編碼經驗的人都知道,程序的正常運行,只是最基本的要求。更多的,還要考慮程序的性能,運行效率,組織結構,和重用性等等。
今天將簡單的討論一下如何優化程序性能。
要寫出高效的程序,可能多數初學者想到的是在程序中用合適的算法和數據結構。這確實是一中提高程序性能的主要方法。
而這里要討論的是另一種方法,也是很多人都忽略但確實很重要的方法。也是我們這篇文章的主題:
如何編寫出編譯器能有效優化的源代碼。
編譯器優化的局限性
沒有萬能的東西,編譯器也一樣。現代編譯器都會對源代碼進行優化,以提高程序的性能。比如linux下的GCC編譯器就能控制優化的等級,優化等級高,對應的程序性能好。對於給定的代碼,編譯器並不能保證能得到最好的性能,它也有局限性。所以才需要程序員能寫出編譯器易於理解和優化的代碼。
編譯器簡單的說是講程序語言代碼翻譯為機器碼,既然是翻譯,就不能改變程序想要表達的意思。所以編譯器對程序總是小心的使用安全的優化。也就是說:優化后的版本和未優化的版本有一樣的行為。
下面簡單說一下編譯器的局限性
1、存儲器別名引用
存儲器別名引用就是兩個不同的指針可能指向存儲器中的同一個位置。
例子說明:看如下的代碼
兩個程序似乎有相同的行為。都是將存儲在*yp處的值兩次加到*xp處存儲的位置。這時,twiddle2的效率會更高一些。(可以認為第二個是第一個的簡單優化)
第一個函數需要6次存儲器引用(讀*xp兩次,寫*xp兩次,讀*yp兩次)而第二個函數只需3次存儲器引用(讀*yp兩次,寫*xp一次)
當然上面討論的是在*xp和*yp指向不同位置的基礎上。
現在考慮*xp和*yp指向同一位置的情況:
函數twiddle1的結果是*xp的值翻四倍,twiddle2得到的是3倍。編譯器並不知道twiddle1會如何被引用,即不知道*xp,*yp是否指向存儲器的同一位置,如果指向同一位置,編譯器就不能把函數1優化為函數2的形式。因為他們有不同的行為。
為了編譯后程序行為不被改變,也就是所謂的安全優化,編譯器只能假設*xp 、*yp 會指向相同的位置。也就是說編譯器不會把函數1優化為函數2的形式。這造成了一個妨礙優化的因素。
2、函數副作用
用例子說明問題,看下面簡單的代碼:
簡單的看兩個函數能產生相同的結果。同樣的,可以暫時認為func2是func1的優化版本。
但是func2 調用了f()一次,而func1調用了f()兩次。如果他們調用的函數f()修改了全局變量,結果就會有所不同
考慮如下f()代碼:
int f()
{
return counter++;
}
這個函數修改了全局變量counter,函數調用的次數會改變程序的行為。也就是說:這個函數有副作用。
大多數編譯器在代碼優化的時候不會試圖判斷一個函數是否有副作用。為了安全的優化,編譯器會認為所有函數都有副作用。
這也成為妨礙編譯器優化的另一因素。
對於函數會有副作用的情況,在寫代碼的時候就要“幫助”編譯器做出判斷。和上面的代碼對應的,如果函數f()沒有副作用,在寫程序的時候就把代碼寫成func2的形式。因為編譯器是不會把func1()優化為func2()形式的。
消除循環的低效率
請看下面的代碼:
看for循環里,里面的判斷條件i<vec_length(v) (不用去管這個函數是干什么用的)我們知道,for循環每次都要判斷 i 的值是否滿足條件,按照上面的代碼也就意味着每次循環都要執行函數vec_length(v)。如果這個函數的值不會因為循環而改變,那么把這個求值過程放在循環外面,只執行一次函數就把值保存起來,會有更好的效果。
改進后的代碼如下:
這是一個常見的代碼優化例子,稱為代碼移動。適用於要執行多次(如在循環里)但計算結果不變的情況。而這類優化是編譯器不能達到的。
編譯器會非常小心,為了安全,它認為所有調用的函數都有副作用。
消除不必要的存儲器引用
考慮下面的代碼:
看被圈起來的部分,OPER表示某種操作,for循環的目的是將數組data[]中的所有值依次執行某種操作並把最后的值存入*dest。
觀察循環內的代碼,下面列出每次循環對存儲器的操作:
1、讀*dest
2、讀data[i]
3、(通過計算)寫*dest
熟悉匯編的可以參看以上代碼的匯編形式:
對於第 i 次循環,第 i 次讀的*dest的值剛好就是第 i-1 次寫入*dest的值。
由此可見,每次對於*dest的寫操作是多余的。因為下一次又會對他寫入覆蓋前面的值,而我們需要的只是最后一次寫入。
考慮到這里,可以用一個臨時變量來記錄*dest 的值,這樣就不用每次循環都重復對*dest的讀寫工作。在循環結束后講臨時變量的值寫入*dest即可。
如上所說,引入臨時變量x的代碼如下
對應的匯編代碼:
小記
本文先討論了編譯器在代碼優化方面的局限性,如簡單的存儲器別名引用和函數副作用。為了安全的優化,編譯器總是考慮最糟的情況,他會認為所有的存儲器引用都會有別名引用,所有的函數都會有副作用。而這兩點成了限制編譯器優化能力的很大因素。所以我們就有責任編寫出編譯器易於優化的代碼。當確定了存儲器沒有別名引用時,或者當確定函數沒有副作用時,適當的修改代碼,能協助編譯器編寫出性能好的程序。
參考:《computer system》
http://en.wikipedia.org/wiki/Program_optimization
不足之處、歡迎拍磚
如轉載請著名出處:http://www.cnblogs.com/yanlingyin/
一條魚 @博客園
2012-2-5