終於談到這個話題了,首先聲明我不是匯編優化的高手,甚至於我知道的所有關於匯編優化的內容,僅僅來自於學校的課程、書本及當年做過的一些簡單練習。換句話說,我了解的東西只能算是一些原則,甚至也有一些“陳舊”了——不過我想既然是一些原則性的東西,還是能夠用它來做一定程度的判斷。至少我認為,我在博客園裡看到的許多關於“匯編優化”也好,“內嵌匯編”也罷的說法,經常是有些問題的。
說到匯編優化,自然被人想到“高性能”。似乎用.NET或Java平台上的程序性能一定不佳,性能好的程序一定要用C++——不,至少一定要用C來寫。為什麼呢?因為一個“常識”:便是 “封裝”會損失性能。性能最高的是“機器碼”,因為CPU直接執行機器嗎;“匯編”作為機器碼的直接對應產物性能自然是一致的;C語言對於匯編/機器碼幾乎沒有任何封裝,因此性能也很好;而到了C++語言時,性能就要比C慢一些了——不過,這個看法正確嗎?
其實我最近這幾篇文章談的都是與程序性能,尤其是代碼執行效率有關的話題。在上一篇文章裡,我們可以知道即便是在使用匯編編寫代碼,同樣繞不開“CPU緩存”這部分與計算機體系結構相關的內容,而它對程序性能的影響甚至遠遠超過幾句指令本身。事實上這也只是一小個方面而已,我們平時在談性能相關問題時,總是在做很多假設,例如我們會假設不同指令的執行速度是一樣的,各級別存儲的讀取性能也是相同的,但這都只是一個“理想環境”,和“事實”有很大差距。而進行匯編級別的優化,往往也是在利用“事實”進行細枝末節的調整。
例如,假設編譯器只是對代碼做“直接翻譯”的話,您認為以下兩種做法性能哪個比較好?
int sum = 0;
for (int i = 0; i < 100; i++)
{
sum += array[i];
}
int sum1 = 0, sum2 = 0;
for (int i = 0; i < 100; i += 2)
{
sum1 += array[i];
sum2 += array[i + 1];
}
int sum = sum1 + sum2;
從算法上看,兩者完全相同,但是對於CPU來說,後一種做法比前一種做法性能要高。首先,第二段代碼與前者相比,一個循環內部有兩個完全不相關的加法運算,這樣CPU便有機會將他們並行地執行,於是性能便會更好一些。其次,第二種做法的條件跳轉次數少,一般來說性能就會更好一些。因為條件跳轉直到最後一刻才知道要跳向何方,因此CPU流水線就很難對代碼的走向進行預測了。當然,現在CPU設計已經引入了分支預測技術,如果預測成功,效率自然較高,但如果預測失敗,那麼便會有比較嚴重的損失了。因此,有時候“我們”會盡可能想辦法去減少條件跳轉的次數。
例如,求一個有符號32位整數的絕對值,按照我們普通的邏輯,它應該是這樣的:
if (eax < 0) eax = -eax
這顯然是一個條件跳轉,但是它的匯編實現也完全可以是:
cdq // 擴展eax的符號位到edx中,如果eax是正數則edx為0否則edx為0xffffffff
xor eax, edx // 如果eax為負數,就把所有的位取反,否則不變
sub eax, edx // 如果最開始eax為負數,則把這個數字取反加一
這樣,原本的條件跳轉消失了,但是我們使用順序的匯編指令得到了正確的結果。
這樣看來,內嵌匯編對於性能多麼關鍵啊。但是,我們真需要親自動手實現這些嗎?無論是前面的“循環展開”還是後面的“取絕對值”都是機械的匯編級別的優化,這些正是編譯器最(包括運行時裡的JIT)擅長的優化手段了。如果我們想要代替編譯器去做這些事情,基本上唯一的結果只是“丑陋的代碼”而難以有性能的提高。
編譯器其實是提高代碼執行效率的重要工具,例如之前在談這個話題的時候,有人談到OCaml的性能比C/C++要高,這便是因為它的編譯器並不需要像C/C++編譯器那樣作出最壞的打算——例如C/C++很多時候無法檢測出兩個變量之間的關系,因此只能按部就班地執行。同樣,我們為什麼說C語言中strlen()不應該放在循環內部,因為它會造成重復計算?因為C語言編譯器不能假設在循環過程中strlen的返回值永遠不變,因此它無法自動將其提取到循環外部,只能一遍遍地執行。
因此很多時候,我們在這方面必須為編譯器做點什麼。例如,一個關於處理器的“常識”便是,不管是整數還是浮點數,除法操作都比乘法要慢上許多,因此我們需要盡可能消除一些除法,例如在進行圖片縮放的時候,我們需要確定縮放的依據是“寬”還是“高”,因此我們可能就會寫這樣的代碼:
if (desiredWidth / originalWidth < desiredHeight / originalHeight)
事實上,如果您要在性能上作精細地追求,則這樣是更好的做法:
if (desiredWidth * originalHeight < desiredHeight * originalWidth)
可惜的是,編譯器可能無法為我們自動作這樣的優化:我們的這些變量都是32為“有符號”整數,因此originalWidth可能會是負數。雖然我們知道圖片的尺寸一定大於零,但是我們卻沒有辦法把這些信息告訴編譯器,因此編譯器只能做最保守的計算了。
看到這裡您可能會說,這些是在談匯編優化嗎?好像還是一直再說高級代碼啊。沒錯,因為正向剛才所說那樣,我不其實並不了解多少匯編優化的內容,我也只能說一些“大道理”。如果您對這方面有些“興趣”的話,雲風的《游戲之旅——我的編程感悟》一書似乎值得您一看(其實這篇文章的許多說法,都和這本書有密切關系)。在這本書裡,雲風總結他在多年游戲開發中總結到的經驗,其中有相當部分便是匯編優化方面的內容。其中也討論了許多其他方面的問題,如文章開始我提到的C++和C語言的性能高低,他認為C++的性能其實與C語言相比有過之而無不及,如果您在C語言裡實現C++的特性(如多態)則幾乎無法作的如C++一樣好,而反過來,如果在C++中做C語言寫過程式的代碼,其性能往往會比C語言來的好。為什麼?語言特性與編譯器的威力呗。
如今的處理器,它的的優化手段已經非常高級,遠不是在加快時鐘頻率上那麼簡單。這給了程序員手動進行匯編優化的動力,因為此時可能只要交換兩條指令的順序便可以有很明顯的性能提高,而編譯器的力量已經不足以作更細致的優化了。同時,CPU設計上的進步也在敦促程序員要不斷更新自己的知識,因為可能在舊CPU上常用的優化方式,到了新的CPU上就不是那麼明顯了。例如《游戲之旅》就用了“不小”的篇幅“簡單”描述了從Pentium到Pentium IV上漸進的優化方式。
當然,我並不贊同以性能為尊的程序編寫方式,事實上匯編優化遠比編寫高級代碼更可能遇到麻煩。雲風在書上也強調,不要過於信任自己的匯編書寫能力,即便像他這樣有豐富經驗的高手也遇到過不少令人大跌眼鏡的事情。
更新:
希望您在看了這篇文章以後,也可以關注一下下面的評論,不乏精妙說法。
文章來源:http://www.cnblogs.com/JeffreyZhao/archive/2010/01/14/talk-about-code-performance-4-asm-optimization.html