from http://blog.csdn.net/prsniper/article/details/7297315
2012.02
C/C++以其飚捍的執(zhí)行效率和近乎ASM的強大功能,使得這類語言在IT技術發(fā)展的驚濤駭浪中,久戰(zhàn)不衰.
C/C++中一個重要的特色就是內(nèi)聯(lián)函數(shù)(在函數(shù)的聲明代碼有inline),那么我們就深入探討一下這個神秘的小家伙吧.
如果跨過高級語言進入?yún)R編層面,函數(shù)的調(diào)用是通過堆棧完成的,往往函數(shù)參數(shù)越多,堆棧操作也越多,完成后堆棧的清理任務也更繁重.而內(nèi)聯(lián)函數(shù)則是面向編譯的小伎倆,直接將內(nèi)聯(lián)函數(shù)的源碼,復制到主調(diào)函數(shù)中,省去了堆棧操作,一定程度上提高了程序的性能(CPU緩存正在改變這一點,我們后面再說),先來看一段簡單的代碼:
又是一個hello world程序.在VC6中我們直接進入DASM進行DEBUG(VC6以上版本也可以的,比如VC.NET2003)
7: int main(int argc, char* argv[]) /*命令行argc:參數(shù)數(shù)量,argv[]參數(shù)數(shù)組,字符串形式*/
8: { void fnHelloWorld2(); //聲明函數(shù)
00401030 push ebp ;基址指針入棧
00401031 mov ebp,esp ;堆棧棧頂指針傳送到基枝指針,其實就是不管前面的堆棧,一切從這里開始
00401033 sub esp,40h ;ESP減少0x40=64字節(jié),這就是函數(shù)的堆棧空間
00401036 push ebx ;存儲器指針入棧
00401037 push esi ;源指針
00401038 push edi ;目的指針
00401039 lea edi,[ebp-40h] ;不改變EBP的情況下,將EBP-0x40的值傳給EDI,見下
0040103C mov ecx,10h ;串操作計數(shù),要填充那么多次
00401041 mov eax,0CCCCCCCCh
00401046 rep stos dword ptr [edi] ;填充堆棧空間為0xCCCCCC,這就是為什么VC的局部變量默認是0xCC...
9:
10: printf("Hello World1!\n"); //常規(guī)調(diào)用
00401048 push offset string "Hello World1!\n" (0042001c) ;字符串指針入棧
0040104D call printf (00401130) ;調(diào)用PRINTF函數(shù)
00401052 add esp,4 ;棧頂指針加一個DWORD,其實就是刪去這個字符串變量
11: fnHelloWorld2(); //call function
00401055 call @ILT+0(fnHelloWorld2) (00401005) ;@ILT我還沒完全弄清楚,不敢亂說,個人認為是便宜常量(見下)
12: fnHelloWorld3(); //call inline function
0040105A call @ILT+10(fnHelloWorld3) (0040100f)
13: return 0;
0040105F xor eax,eax ;XOR自身其實就是將自身清0
14: }
00401061 pop edi ;壓入堆棧的寄存器,依次復原,又是一堆羅嗦的操作
00401062 pop esi
00401063 pop ebx
00401064 add esp,40h
00401067 cmp ebp,esp
00401069 call __chkesp (004011b0) ;堆棧檢查,你可以繼續(xù)跟蹤,好長...
0040106E mov esp,ebp
00401070 pop ebp
00401071 ret ;終于函數(shù)調(diào)用結(jié)束了
....
@ILT+0(?fnHelloWorld2@@YAXXZ):
00401005 jmp fnHelloWorld2 (00401090) ;跳轉(zhuǎn)到這里,相當于直接執(zhí)行PRINTF了,中間只經(jīng)歷了2個JMP無條件跳轉(zhuǎn)
@ILT+5(_main):
0040100A jmp main (00401030)
@ILT+10(?fnHelloWorld3@@YAXXZ):
0040100F jmp fnHelloWorld3 (004010e0)
現(xiàn)在,我們可以看到,內(nèi)聯(lián)的函數(shù)其實就是很卑鄙地給函數(shù)執(zhí)行部分加一個標簽,調(diào)用的時候直接跳轉(zhuǎn)到這里執(zhí)行.
省去了一大堆堆棧操作,然而是否可以任意使用inline呢?答案是否定的,再看一段代碼(原代碼捎加修改而已):
我們在C/C++層面DEBUG,發(fā)現(xiàn)循環(huán)中bv[i] = b[i];沒有生效,實際執(zhí)行中也是,那我們就看看它更深層的機理吧:
17: void fnHelloWorld2(unsigned char *b/*b[128]*/)
18: { int i;
0040B820 push ebp ;相同的地方就不贅述了
0040B821 mov ebp,esp
0040B823 sub esp,0C4h ;這里用更大的堆棧空間0xC4=196,是為了能容下局部變量
0040B829 push ebx
0040B82A push esi
0040B82B push edi
0040B82C lea edi,[ebp-0C4h]
0040B832 mov ecx,31h
0040B837 mov eax,0CCCCCCCCh
0040B83C rep stos dword ptr [edi]
19: unsigned char bv[128];
20: printf("Hello World:");
0040B83E push offset string "Hello World:" (00420034)
0040B843 call printf (00401130) ;先執(zhí)行一次PRINTF
0040B848 add esp,4
21: for(i = 0; i < 128; i++) //下面就是循環(huán)編譯后的匯編代碼啦
0040B84B mov dword ptr [ebp-4],0 ;基址指針-4就是變量i,初始化為0
0040B852 jmp fnHelloWorld2+3Dh (0040b85d) ;轉(zhuǎn)到CMP
0040B854 mov eax,dword ptr [ebp-4] ;經(jīng)過循環(huán)來到這里,將i的值傳送到EAX累加器,自加(i++)
0040B857 add eax,1
0040B85A mov dword ptr [ebp-4],eax ;結(jié)果回到i的地址,完成i++
0040B85D cmp dword ptr [ebp-4],80h ;i與0x80=128比較
0040B864 jge fnHelloWorld2+72h (0040b892) ;如果i大于或者等于128則跳轉(zhuǎn)到循環(huán)以后執(zhí)行源碼第25行
22: { printf("%d,", b[i]); //打印沒有問題,就不解釋了,偷懶就是藝術
0040B866 mov ecx,dword ptr [ebp+8]
0040B869 add ecx,dword ptr [ebp-4]
0040B86C xor edx,edx
0040B86E mov dl,byte ptr [ecx]
0040B870 push edx
0040B871 push offset string "%d," (00420044)
0040B876 call printf (00401130)
0040B87B add esp,8
23: bv[i] = b[i];
0040B87E mov eax,dword ptr [ebp+8] ;這里解釋一下EBP[=調(diào)用前ESP]是堆棧,-4是第一個局部變量,+8則是第一個參數(shù)
0040B881 add eax,dword ptr [ebp-4] ;第一個參數(shù)的地址+變量i的偏移得到b[i]
0040B884 mov ecx,dword ptr [ebp-4] ;一樣計算偏移,存儲在ECX中,下面將作為局部變量BV的索引
0040B887 mov dl,byte ptr [eax] ;EDX的低8位存儲參數(shù)1對應偏移的值 b[i]
0040B889 mov byte ptr [ebp+ecx-84h],dl ;EBP-i-0x80-4這樣好理解多(我看了下,如果字節(jié)倒序存儲是沒有問題的,但是內(nèi)存地址的值沒有被更改)
24: }
0040B890 jmp fnHelloWorld2+34h (0040b854) ;跳轉(zhuǎn)回到累加操作,準備下一個循環(huán)
25: printf("\b!\n", i);
0040B892 mov eax,dword ptr [ebp-4]
0040B895 push eax
0040B896 push offset string "\x08!\n" (00420030)
0040B89B call printf (00401130)
0040B8A0 add esp,8
26: }
0040B8A3 pop edi
0040B8A4 pop esi
0040B8A5 pop ebx
0040B8A6 add esp,0C4h
0040B8AC cmp ebp,esp
0040B8AE call __chkesp (004011b0)
0040B8B3 mov esp,ebp
0040B8B5 pop ebp
0040B8B6 ret
代碼不能亂用,就像女人不能亂碰,否則在特定的情況下,讓你痛不欲生!
網(wǎng)上有資料說:不能包含循環(huán)、switch、if語句,我沒有完整實驗,不過循環(huán)是不行了,至于你信不信,反正我信了.
下面回到開頭,我們說的CPU緩存在改變這一點,做一個簡單的敘述:
直接的說,現(xiàn)在一般CPU的緩存比較大,如我的爛U:AMD雙核 6000+ 分別有64K的L1(Level1)數(shù)據(jù)和代碼緩存,好一點的可以到256K,我的L2有2M的數(shù)據(jù)代碼緩存.
通常來講,L1的存取速度直接就是寄存器的速度,理論可以達到CPU時鐘頻率,可以看作是不需要時間的,而內(nèi)存一般是每秒10G左右的讀寫速度.
CPU將一些代碼從內(nèi)存緩存,再需要的時候不再訪問內(nèi)存,如果內(nèi)聯(lián)函數(shù)過多,將導致緩存承受不了那么多的空間,而反復緩存,就像WEB開發(fā)中的緩存,重復緩存還不如直接從磁盤讀取..
inline函數(shù)有它強悍的地方,用于高效程序,加密等可以說是"牛X",然而鑒于上面的困境,有沒有什么好的方法呢?
方法當然是有:
1.自己把內(nèi)聯(lián)函數(shù)的過程再寫一遍(累,跟主調(diào)函數(shù)融合難)
2.改為非內(nèi)聯(lián)函數(shù)(吐了)
3.自己寫匯編代碼(這個是我想到的比較好的方法了 - -)