[旧帖]
[原创]virtual函数调用的代价
0.00雪花
发表于:
2010-1-21 23:10
2603
[旧帖] [原创]virtual函数调用的代价
0.00雪花
定义一个类。
view plaincopy to clipboardprint?
class CA
{
public:
void FunA()
{
};
};
class CA
{
public:
void FunA()
{
};
};
通过指针调用FunA。
view plaincopy to clipboardprint?
CA* pa = new CA;
pa->FunA();
CA* pa = new CA;
pa->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,会发生什么呢?
view plaincopy to clipboardprint?
class CA
{
public:
virtual void FunA()
{
};
};
class CA
{
public:
virtual void FunA()
{
};
};
// 看看汇编吧。
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还是不要为好。
个人意见,欢迎拍砖。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)