在Ailiss社區與人爭論語法,終極武器不外乎兩把 —— 上天入地。
上天者,搬出枕頭厚的大部頭引經據點,說有Lipman某典故云云;又有C++標準M頁N條款如是說...
入地者,操起起子扳手把程序拆個凄涼八落,啪啪啪回上一大片編譯器匯編的輸出,說——看吧,都在這里了。
事實上,我發覺兩者對于深入理解C++都是必不可少的。前些時,
論壇上突然流行討論數組的本質。怪的是,每次我以為自己真的懂了,下一次又卻發現自己的輕浮。等到通曉實現細節,再回過頭來看最土的C教材的定義,竟發現字字璣珠。
最近對入地頗感興趣,動輒查匯編,寫了不少混合代碼研究語法。放下扳手抹抹鼻尖上的機油,倒是有些心得。各位看官——我就要跟大家亂侃近來入地一個多月,邪門歪地道挖出雜七雜八的東西——的
工具——就 匯編。
——喝口茶先,大家先看看秋鎮菜blog上這篇文章《在 Visual C++ 中使用內聯匯》 ,詳細介紹了在 Visual C++ 中內聯匯編的用法。
參考書也必不可少;可悲的是我手頭僅有的兩本書一本是老掉牙的8086匯編,另一本則是AT&T語法的 —— 廣泛用于
Linux上的編譯器, 但VC 偏要使用Inter語法.... 帶來麻煩不少,希望大家慷慨解囊之前先看準。
--------------------------------------------------------------------------------
1 察看編譯器輸出
通常來說,Debug 模式單步跟蹤時Alt+8 就可以看見匯編代碼。問題是 Debug 只是代表了一個側面,并不代表最終的 Release ;另一方面 Debug 模式包含了些許額外的測試代碼 —— 恩,可能代碼有些多...天啊,他們干嘛要加、那么多、莫名其妙的代碼混淆視聽阿!
好嘛,看看簡潔的Release模式 —— orz.... 不能單步跟蹤C++程序了? 連main函數在哪里都看不見... 瞎了...
Release 模式單步跟蹤要需要高深的技術底氣。不過也沒那么絕,要看 Release 模式的輸出,我們可以在項目屬性->C/C++->輸出文件頁面中把“匯編輸出”項定為“帶源代碼的程序集(/FAs)”。這樣,在Release目錄下就可以看見對應的asm文件了。看asm文件,唯一的缺點是不能單步跟蹤研究。
這個asm文件搞不好會非常大——主要是由于C++標準庫廣泛使用模板的原因,若我們放棄C++庫一律使用C標準庫就會看到很干凈的asm文件(同時會看見一個1/4大小的可執行文件,你會明白為什么那么多人支持C )——當然這不是C++的干活。 要在這個動輒數W行的文件中里面找源代碼對應的匯編,推薦大家找一行一定不會被優化掉的代碼(沒錯,某些代碼可能人間蒸發),直接F3搜索。
asm中包含了很多注釋,有基本的匯編
知識然后連蒙帶猜就能看懂了。一對挺有用的標志是:
_TEXT SEGMENT // 代碼段開始標志
_TEXT ENDS // 代碼段結束標志
對于觀察每個函數的生成代碼來說,這兩個標志能起到路標的作用。
--------------------------------------------------------------------------------
2 匯編訪問類成員
若有一個類
class A{
int _i;
};
有A 的實例a,下面的代碼令 a._i = 10,這只需要一個指令:
__asm mov [a]A._i, 10
但是在A 的成員函數中怎么辦呢?
我們知道,成員函數調用為 thiscall, this通過 ecx傳遞。所以在函數的開頭現場尚未被破壞的時候,可以直接用 ecx 變址訪問。如下面是一個常見的set函數, 它令 A::_i = n (注意mov等指令中,兩個操作數不能同時為內存內容,所以必須用寄存器eax接力):
inline void A::i( int n ){
__asm mov eax, n
__asm mov [ecx]A._i, eax
}
不過這有兩個問題。一來,ecx并非總是this;它隨時可能被刷掉。在某個不能確定保存this寄存器的時候,你需要手動寫ecx:
__asm mov ecx, this
__asm mov eax, n
__asm mov [ecx]A._i, eax
這樣寫會迫使編譯器把this的值復制到棧上 —— 而一般來說對于小函數而言,編譯器會盡量只用寄存器。這可能是一個額外的小小開銷。(注意,千萬不要以為可以這么訪問: [this]A._i )
另一方面,雖然在我們的確寫了大大的“ inline ”幾個字,但是看看輸出代碼——你會發現:任何包含了內嵌匯編的 inline 成員函數都不會被內聯!
--------------------------------------------------------------------------------
3 匯編/內聯函數和效率
普通函數是可以內聯的,下面就是一個完美的結合 C++/ asm 的例子:
inline long long getTimer(){
long long time;
__asm rdtsc
__asm mov DWORD PTR time, eax
__asm mov DWORD PTR time + 4, edx
return time;
}
rdtsc指令用來獲得CPU自
開機運行的時鐘周期數。它的結果是64位的,保存到 eax 和 edx兩個寄存器中,可以用來精確測量算法開銷。上面的函數內聯之后, 局部變量不見了, 臨時返回值也不見了,只有最核心的三行代碼,沒有比這更簡潔的了:
; 68 : long long b = getTimer();
rdtsc
mov DWORD PTR _time$11298[ebp], eax
mov DWORD PTR _time$11298[ebp+4], edx
成員函數內聯則又是另一個故事:系統不知道如何處理this,所以他干脆忽略所有內嵌asm成員函數的內聯標志。
好嘛,VC不愿上,我們用皮鞭趕著他上! 把第二部分最初那個 A::i 改為 __forceinline 就強制內聯了——也就是強制VC犯錯誤了:不幸的編譯器看不懂我們的代碼,只好把指令抄到函數調用處。他不曉得初始化ecx,那個mov可能往任何地方寫內容——比如把你的開機
密碼寫到桌面上——
雖然可以手動設置ecx,不過我們可不希望看見如此丑陋的調用(想象一下你的同事看到這段代碼的困惑):
__asm lea ecx, a
a.i( 20 );
要正確編寫能成功內聯的代碼必須結合另一個方案,手動復制this:
__forceinline void A::i( int n ){
__asm mov ecx, this
__asm mov eax, n
__asm mov [ecx]A._i, eax
}
厄。。。猜猜看結果如何?
首先看看我們直接用C++寫一個 set函數 (譬如 void A::i( int n ){ _i = n; } )內聯后的結果:
; 56 : a.i( 5 );
mov DWORD PTR _a$[ebp+8], 5
最殘酷的結果也只需一句mov。 更可能的結果是——他被優化得連影兒都看不見。
然后看看我們的三年懷胎含辛茹苦研究出來的混合匯編的內聯:
; 56 : a.i( 5 );
lea eax, DWORD PTR _a$[ebp]
pop ecx
mov DWORD PTR $T11194[ebp], eax
mov ecx, DWORD PTR $T11194[ebp]
mov eax, 5
mov DWORD PTR [ecx+8], eax
這么長啊....生出一個怪胎... VC 中嵌入匯編的一個壞處是:編譯器很難將他和C++協調,很難優化他。
匯編優化可以很快速、很強,但是一定要慎用。
posted on 2008-07-23 16:28
chatler 閱讀(135)
評論(0) 編輯 收藏 引用 所屬分類:
C++_BASIS