现在的位置: 首页 > 综合 > 正文

virtual函数调用的代价

2013年09月04日 ⁄ 综合 ⁄ 共 3238字 ⁄ 字号 评论关闭

定义一个类。

 

 

通过指针调用FunA。

 

此时,转入汇编,我们看看pa->FunA()到底是怎么做的。

pa->FunA();

// 汇编展开如下:

004113E7  mov         ecx,dword ptr [pa]

// 函数调用。
004113EA  call        CA::FunA (411005h)
// 看看FunA做了什么。中间做了一个跳转

00411005  jmp         CA::FunA (411420h)

// 继续展开。

 void FunA()
 {

// EBP压栈,为函数调用保护现场。
00411420  push        ebp 
00411421  mov         ebp,esp
00411423  sub         esp,0CCh
00411429  push        ebx 
0041142A  push        esi 
0041142B  push        edi 
0041142C  push        ecx 

//  函数FunA并没有任何操作,为什么汇编展开成这么多,我暂时也不知道为什么。
0041142D  lea         edi,[ebp-0CCh]
00411433  mov         ecx,33h
00411438  mov         eax,0CCCCCCCCh
0041143D  rep stos    dword ptr es:[edi]
0041143F  pop         ecx 
00411440  mov         dword ptr [ebp-8],ecx
 };

// 函数返回
00411443  pop         edi 
00411444  pop         esi 
00411445  pop         ebx 
00411446  mov         esp,ebp
00411448  pop         ebp 
00411449  ret           

至此。函数调用完成。

 

可是如果我们为FunA声明为virtual,会发生什么呢?

 

// 看看汇编吧。

 pa->FunA();

// 调用咱开。
0041146D  mov         eax,dword ptr [pa]
00411470  mov         edx,dword ptr [eax]
00411472  mov         esi,esp
00411474  mov         ecx,dword ptr [pa]

// 将虚函数的地址,从虚表中取出来,放入到EAX
00411477  mov         eax,dword ptr [edx]

// 调用虚函数。下面这一句才是真铮的调用FunA。

// 前面的调用均是准备工作,

// 目的是将FunA在CA对象的虚表指针指向的虚表中的地址取出来。
00411479  call        eax 

// 安全性检查,由于是Debug下运行,如下汇编会在release会优化
0041147B  cmp         esi,esp
0041147D  call        @ILT+355(__RTC_CheckEsp) (411168h)

 

对00411479  call        eax  这一句汇编展开。

CA::FunA:
00411005  jmp         CA::FunA (411510h)

 virtual void FunA()
 {
00411510  push        ebp 
00411511  mov         ebp,esp
00411513  sub         esp,0CCh
00411519  push        ebx 
0041151A  push        esi 
0041151B  push        edi 
0041151C  push        ecx 
0041151D  lea         edi,[ebp-0CCh]
00411523  mov         ecx,33h
00411528  mov         eax,0CCCCCCCCh
0041152D  rep stos    dword ptr es:[edi]
0041152F  pop         ecx 
00411530  mov         dword ptr [ebp-8],ecx
 };
00411533  pop         edi 
00411534  pop         esi 
00411535  pop         ebx 
00411536  mov         esp,ebp
00411538  pop         ebp 
00411539  ret        

后面的进入到函数FunA内部,与非virutual的函数调用汇编一致。

 

上述汇编均在Debug模式下展开。

可以发现,使用virtual的主要效率损失是在函数调用的准备阶段,

我们需要在对象的虚表指针的某一个位置取出函数实际地址,继而转入函数地址进行函数调用。

在Release模式下,我们看看函数调用的准备阶段到底发生了什么。

 

不使用virtual:

在本机上FunA的调用直接被优化掉了。断点无法命中。

于是我觉得需要给他加一些有意义的代码,我们只看函数调用准备阶段到底做了什么。

pa->FunA();

// 展开时就是一个call指令,直接指向函数地址。
0040100A  call        dword ptr [__imp__rand (4020A4h)]

 

 

使用virtual:

 pa->FunA();
00401024  mov         edx,dword ptr [eax]
00401026  mov         ecx,eax
00401028  mov         eax,dword ptr [edx]
0040102A  call        eax 

// 对call调用展开,发现该函数就是一个ret指令。因为该函数并没有任何有意义的执行语句。

 virtual void FunA()
 {
 };
00401000  ret             
--- No source file -------------------------------------------------------------
00401001  int         3   
00401002  int         3   
00401003  int         3   
00401004  int         3   
00401005  int         3   
00401006  int         3   
00401007  int         3   
00401008  int         3   
00401009  int         3   
0040100A  int         3   
0040100B  int         3   
0040100C  int         3   
0040100D  int         3   
0040100E  int         3   
0040100F  int         3   
--- e:/workspace/virtualdemo/virtualdemo/virtualdemo.cpp -----------------------
};

 

结论:

不使用virtual,一个成员函数的调用只需要一条call指令。

如果使用virtual,一个成员函数调用,需要额外的三条mov指令来

 

呵呵,当我们在享用virtual带来的多态时,

也带来了一些效率上的损失。

同时virtual也会抑制inline以及调用优化。

 

对于不必要的virtual还是不要为好。

 

个人意见,欢迎拍砖。

 

抱歉!评论已关闭.