linux 虚函数调用性能,C++对象布局及多态实现探索之虚函数调用
我們?cè)倏纯刺摮蓡T函數(shù)的調(diào)用。類C041中含有虛成員函數(shù),它的定義如下:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
執(zhí)行如下代碼:
C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
obj.foo();
C041 * pt = &obj;
pt->foo();
結(jié)果如下:
The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
我們打印出了C041的對(duì)象內(nèi)存布局及它的虛表信息。
先看看obj.foo();的匯編代碼:
004230DF lea ecx,[ebp+FFFFF948h]
004230E5 call 0041DF1E
和前一篇文章中看過的普通的成員函數(shù)調(diào)用產(chǎn)生的匯編代碼一樣。這說明了通過對(duì)象進(jìn)行函數(shù)調(diào)用,即使被調(diào)用的函數(shù)是虛函數(shù)也是靜態(tài)綁定,即在編譯時(shí)決議出函數(shù)的地址。不會(huì)有多態(tài)的行為發(fā)生。
我們跟蹤進(jìn)去看看函數(shù)的匯編代碼。
01 004263F0 push ebp
02 004263F1 mov ebp,esp
03 004263F3 sub esp,0CCh
04 004263F9 push ebx
05 004263FA push esi
06 004263FB push edi
07 004263FC push ecx
08 004263FD lea edi,[ebp+FFFFFF34h]
09 00426403 mov ecx,33h
10 00426408 mov eax,0CCCCCCCCh
11 0042640D rep stos dword ptr [edi]
12 0042640F pop ecx
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax+4],2
16 0042641A pop edi
17 0042641B pop esi
18 0042641C pop ebx
19 0042641D mov esp,ebp
20 0042641F pop ebp
21 00426420 ret
值得注意的是第14、15行。第14行把this指針的值移到eax寄存器中,第15行給類的第一個(gè)成員變量賦值,這時(shí)我們可以看到在取變量的地址時(shí)用的是[eax+4],即跳過了對(duì)象布局最前面的4字節(jié)的虛表指針。
接下來我們看看通過指針進(jìn)行的虛函數(shù)調(diào)用pt->foo();,產(chǎn)生的匯編代碼如下:
01 004230F6 mov eax,dword ptr [ebp+FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp+FFFFF900h]
05 00423106 call dword ptr [edx]
第1行把pt指向的地址移入eax寄存器,這樣eax中就保存了對(duì)象的內(nèi)存地址,同時(shí)也是類的虛表指針的地址。第2行取eax中指針指向的值(注意不是 eax的值)到edx寄存器中,實(shí)際上也就是虛表的地址。執(zhí)行完這兩條指令后,我們看看eax和edx中的值,果然和我們前面打印的obj的虛表信息中的 vpadr和vtadr的值是一樣的,分別為0x0012F824和0x0045B314。第4行同樣用ecx寄存器來保存并傳遞對(duì)象地址,即 this指針的值。第5行的call指令,我們可以看到目的地址不象通過對(duì)象來調(diào)用那樣,是一個(gè)直接的函數(shù)地址。而是將edx中的值做為指針來進(jìn)行間接調(diào)用。前面我們已經(jīng)知道edx中存放的實(shí)際是虛表的地址,我們也知道虛表實(shí)際是一個(gè)指針數(shù)組。這樣第5行的調(diào)用實(shí)際就是取到虛表中的第一個(gè)條目的值,即 C041::foo()函數(shù)的地址。如果被調(diào)用的虛函數(shù)對(duì)應(yīng)的虛表?xiàng)l目的索引不是0,將會(huì)看到edx后加上一個(gè)索引號(hào)乘4后的偏移值。繼承跟蹤可以發(fā)現(xiàn), ptr[edx]的值為0x0041DF1E,也和我們打印的vtival(0)的值相同。前面已經(jīng)提到過,這個(gè)地址實(shí)際也不是真正的函數(shù)地址,是一個(gè)跳轉(zhuǎn)指令,繼續(xù)執(zhí)行就到了真正的函數(shù)代碼部分(即前面列出的代碼)。
我們?cè)谏厦婵吹降倪@個(gè)過程,就是動(dòng)態(tài)綁定的過程。因?yàn)槲覀兪峭ㄟ^指針來調(diào)用虛成員函數(shù),所以會(huì)產(chǎn)生動(dòng)態(tài)綁定,即使指針的類型和對(duì)象的類型是一樣的。為了保證多態(tài)的語義,編譯器在產(chǎn)生call指令時(shí),不象靜態(tài)綁定時(shí)那樣,是在編譯時(shí)決議出一個(gè)確定的地址值。相反它是通過用發(fā)出調(diào)用的指針指向的對(duì)象中的虛指針,來迂回的找到對(duì)象所對(duì)應(yīng)類型的虛表,及虛表中相應(yīng)條目中存放的函數(shù)地址。這樣具體調(diào)用哪個(gè)函數(shù)就與指針的類型是無關(guān)的,只與具體的對(duì)象相關(guān),因?yàn)樘撝羔樖谴娣旁诰唧w的對(duì)象中,而虛表只和對(duì)象的類型相關(guān)。這也就是多態(tài)會(huì)發(fā)生的原因。
請(qǐng)回憶一下前面討論過的C071類,當(dāng)子類重寫從父類繼承的虛函數(shù)時(shí),子類的虛表內(nèi)容的變化,及和父類虛表內(nèi)容的區(qū)別(請(qǐng)參照第二篇中打印的子類和父類的虛表信息)。具體的通過指向子類對(duì)象的父類指針來調(diào)用被子類重寫過的虛函數(shù)時(shí)的調(diào)用過程,請(qǐng)有興趣的朋友自己調(diào)試一下,這里不再列出。
另外前面在《C++對(duì)象布局及多態(tài)實(shí)現(xiàn)之動(dòng)態(tài)和強(qiáng)制轉(zhuǎn)換》中我們討論了指針的類型動(dòng)態(tài)轉(zhuǎn)換。我們?cè)谶@里再利用C041、C042及C051類,來看看指針的類型動(dòng)態(tài)轉(zhuǎn)換。這幾個(gè)類的定義請(qǐng)參見前文。類C051從C041和C042多重繼承而來,且后兩個(gè)類都有虛函數(shù)。執(zhí)行如下代碼:
C051 obj;
C041 * pt1 = dynamic_cast(&obj);
C042 * pt2 = dynamic_cast(&obj);
pt1->foo();
pt2->foo2();
第一個(gè)動(dòng)態(tài)轉(zhuǎn)型對(duì)應(yīng)的匯編代碼為:
00404B59 lea eax,[ebp+FFFFF8ECh]
00404B5F mov dword ptr [ebp+FFFFF8E0h],eax
因?yàn)椴恍枰{(diào)整指針位置,所以很直接,取出對(duì)象的地址后直接賦給了指針。
第二個(gè)動(dòng)態(tài)轉(zhuǎn)型牽涉到了指針位置的調(diào)整,我們來看看它的匯編代碼:
01 00404B65 lea eax,[ebp+FFFFF8ECh]
02 00404B6B test eax,eax
03 00404B6D je 00404B7D
04 00404B6F lea ecx,[ebp+FFFFF8F1h]
05 00404B75 mov dword ptr [ebp+FFFFF04Ch],ecx
06 00404B7B jmp 00404B87
07 00404B7D mov dword ptr [ebp+FFFFF04Ch],0
08 00404B87 mov edx,dword ptr [ebp+FFFFF04Ch]
09 00404B8D mov dword ptr [ebp+FFFFF8D4h],edx
代碼要復(fù)雜的多。&obj運(yùn)算后得到的是一個(gè)指針,前三行指令就是判斷這個(gè)指針是否為NULL。奇怪的是第4行并沒有根據(jù)eax中的地址(即對(duì)象的起始地址)來進(jìn)行指針的位置調(diào)整,而是直接把[ebp+FFFFF8F1h]的地址取到ecx寄存器中。第1行指令中的[ebp+ FFFFF8ECh]實(shí)際是得到對(duì)象的地址,ebp所加的那個(gè)數(shù)實(shí)際是個(gè)負(fù)數(shù)(補(bǔ)碼)也就是對(duì)象的偏移地址。對(duì)比兩個(gè)數(shù)發(fā)現(xiàn)相差5字節(jié),這樣實(shí)際上第4行是直接得到了指針調(diào)整后的地址,即將指針指向了對(duì)象中的屬于C042的部分。后面的代碼又通過一個(gè)臨時(shí)變量及edx寄存器把調(diào)整后的指針值最終存到了 pt2指針中。
這些代碼實(shí)際可以優(yōu)化成二行:
lea eax, [ebp+FFFFF8F1h]
mov dword ptr [ebp+FFFFF8d4h], eax
我們?cè)岬紺051類有兩個(gè)虛表,相應(yīng)對(duì)象中有也兩個(gè)虛表指針,之所以不合并為一個(gè),就是為了處理指針的類型動(dòng)態(tài)轉(zhuǎn)換。結(jié)合前面對(duì)于多態(tài)的討論,我們就可以理解得更清楚了。pt2->foo2();調(diào)用時(shí),對(duì)象的類型還是C051,但經(jīng)過指針動(dòng)態(tài)轉(zhuǎn)換pt2指向了對(duì)象中屬于C042的部分的起始,也就是第二個(gè)虛表指針。這樣在進(jìn)行函數(shù)調(diào)用時(shí)就不需要再做額外的處理了。我們看看pt1->foo();及pt2->foo2 ();產(chǎn)生的匯編碼即知。
01 00404B93 mov eax,dword ptr [ebp+FFFFF8E0h]
02 00404B99 mov edx,dword ptr [eax]
03 00404B9B mov esi,esp
04 00404B9D mov ecx,dword ptr [ebp+FFFFF8E0h]
05 00404BA3 call dword ptr [edx]
06 00404BA5 cmp esi,esp
07 00404BA7 call 0041DDDE
08 00404BAC mov eax,dword ptr [ebp+FFFFF8D4h]
09 00404BB2 mov edx,dword ptr [eax]
10 00404BB4 mov esi,esp
11 00404BB6 mov ecx,dword ptr [ebp+FFFFF8D4h]
12 00404BBC call dword ptr [edx]
13 00404BBE cmp esi,esp
14 00404BC0 call 0041DDDE
前7行為pt1->foo();,后7行為pt2->foo2();。唯一不同的是指針指向的地址不同,調(diào)用機(jī)制是一樣的。
總結(jié)
以上是生活随笔為你收集整理的linux 虚函数调用性能,C++对象布局及多态实现探索之虚函数调用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 支付宝信用卡收款怎么关闭?这些事项要注意
- 下一篇: linux+shell+func,Lin