Visual C++ 中的去虚拟化
Posted
技术标签:
【中文标题】Visual C++ 中的去虚拟化【英文标题】:Devirtualization in visual c++ 【发布时间】:2012-10-18 20:04:21 【问题描述】:Visual C++ 是否为只有一个实现的纯类去虚拟化函数? 例如:
class ICar
public:
virtual void Break() = 0;
;
class CarImpl : public ICar
public:
virtual void Break() ....
;
【问题讨论】:
我不熟悉去虚拟化这个词,你问是否有运行时查找成本? 去虚拟化正在撤消虚拟命令。它使函数可供使用。但是这里的措辞有点棘手......你的意思是“它在使用时会自动去虚拟化它吗?” 除非它发生在链接时,否则执行此优化永远不会安全——因为如果另一个翻译单元包含从ICar
派生的第二个类怎么办?我知道的唯一优化是,如果在编译时知道对象的动态类型,编译器将使用非虚拟调用(或内联它)。例如:ICar *x = new CarImpl; x->Break();
几乎肯定会使用直接调用。
确保你的车也有一个 Brake() 函数,所以它不会一直 Break()。
@j-random-hacker:是的,我在 VS 2010 和 g++ 4.6 中都做到了。但你是对的,这可以优化。
【参考方案1】:
OP 问题自然分为 3 个问题:
-
VC++ 2010 是否进行了所描述的去虚拟化 - 答案是:
没有
可以做到吗?答案是:可以理论上可以做到
在某些情况下完成。
为什么 VC++ 不这样做。 - 我们只能推测...
以下是详细信息:
1。这样的优化是VC++做的吗
为了证明这个优化没有完成,我们需要在 Project Properties/Configuration Properties/C/C++/Output Files 中启用汇编语言列表:将 Assembler Output 设置为“Assembly With Source Code”(/FAs)”。
这是来自 OP 的稍微修改的 C++ 代码(我将 ICar 从抽象类更改为普通类,它不会改变问题的要点):
#include "stdafx.h"
class ICar
public:
virtual void Accelerate()printf("%s", "a\n");;
virtual void Break()printf("%s", "b\n");;
;
class CarImpl : public ICar
public:
virtual void Accelerate() printf("%s", "accelerate\n");
virtual void Break() printf("%s", "break\n");
void Fly() printf("%s", "fly\n");
;
int _tmain(int argc, _TCHAR* argv[])
ICar *pCar = new CarImpl();
pCar->Break();
CarImpl *pCarImpl = new CarImpl();
pCarImpl->Fly();
CarImpl carImpl;
carImpl.Break();
carImpl.Fly();
return 0;
首先,(注意 1)让我们注意到carImpl.Break();
不使用虚函数。这不是优化的结果——它是 C++ 的一个特性:如果在编译期间知道对象的类型,则不使用虚函数机制。虚函数机制仅在涉及指针或引用时使用。
其次,让我们启用优化 /O2 并查看为pCar->Break();
(虚拟方法)和pCarImpl->Fly();
(非虚拟方法)生成的汇编程序。
对于 Break() 的调用,我们将看到:
; 24 : pCar->Break();
mov edx, DWORD PTR [eax]
mov ecx, eax
mov eax, DWORD PTR [edx+4]
call eax
EAX 包含一个指向 CarImpl 对象的指针(从前面未显示的汇编程序行可以清楚地看出)。在第一条mov指令中,CarImpl对象的第一个dword被加载到EDX中(对象的第一个dword通常是vtbl的地址),然后CarImpl的this
被加载到ECX中(这对我们来说并不重要),然后将EDX指向的点(虚函数表中的第二个函数)偏移4的dword加载到EAX中,然后调用完成。
在 Fly() 的情况下,我们将看到:
; 27 : pCarImpl->Fly();
push OFFSET ??_C@_04PPJAHJOB@fly?6?$AA@
push OFFSET ??_C@_02DKCKIIND@?$CFs?$AA@
call _printf
这只是 printf 的内联,其中传递了两个参数。
因此,显然在 Break() 的情况下,vtable 的使用并未优化。
2。能不能做这样的优化
原则上它可以被优化。我在 M.Ellis, B. Stroustrup, Addison-Wesley 1990 的“The Annotated C++ Reference Manual”中找到了以下语句:第 10.2 章(我有这本书的翻译,我正在翻译回英文 :-) 所以它可能不是 Stroustroup 的确切措辞。)
如果在编译时知道对象的确切类型,则不需要虚函数机制。相反,实现可以生成类成员函数的普通调用。 (DK:我们代码中 carImpl.Break() 的情况,请参阅我的注释 1) ... 当通过指针或引用调用虚函数时,可能无法静态知道对象的实际类型,因此应该使用虚函数的机制。对控制流有足够了解的编译器可以放弃对虚函数的调用,即使在某些情况下,例如通过以下代码中的 bp 调用:
struct base
virtual void vf1();
class derived : public base
public:
void vf1();
void g()
derived d;
base* bp = &d;
bp->vf1();
... 内联虚函数非常有意义并且经常使用。自然,内联仅用于将内联函数应用于已知类型的对象的地方。 (DK:我认为这里 B. Stroustrup 也参考了我们的 carImpl.Break() 案例;即 NOTE1 中描述的案例)。
3。为什么没有完成
虽然这在 OP 中并没有逐字询问,但也许这是一个隐藏的问题。我同意 Alex Cohn 的其中一个 cmets(说得好):
可以,但不能。可能这种情况不会经常发生,不足以证明可靠地优化此类调用所需的资源是合理的。
【讨论】:
大部分都很好,除了你的Test(carImpl);
与虚函数无关——该函数调用实际上展示的是一种称为对象切片的现象,这真的是C++ 本身存在一个隐蔽的设计问题:将派生类传递给采用 按值 基类的函数会导致所有派生成员被切掉,并且对象被视为基类实例。跨度>
@j_random_hacker:感谢这个术语对象切片!正是我的观点是,在静态已知类型的对象上调用方法,例如在 Test 的参数 ICar car
上,或者另一个例子是像 CarImpl ci; ((ICar)ci).Break();
这样的强制转换与虚函数无关。在这个静态已知类中声明的函数将始终被使用——而不是通过 vtable 进行多态替换。只是把它说清楚。 :-) 有趣的是,在 C# 中类似的代码(带有传递参数或强制转换)将调用多态方法。
我仍然认为您通过使用导致对象切片(一个严重而微妙的问题)的示例来说明虚函数调用是在搅浑水。 carImpl.Break();
已经是你所说的一个很好的(不混淆的)例子。
@j_random_hacker:你说得有道理。我已经删除了 Test() 函数。
我用 GCC 测试了测试用例,它被优化为一系列 put 并调用了 new。所以看来优化确实有效。【参考方案2】:
编辑:这个答案已被我 2012 年 10 月 20 日的第二个答案所取代。我没有删除它以保留 cmets。
VC++ 不可能,因为其他派生类可以链接到已经编译好的 dll 或 exe 模块。
【讨论】:
没错,但 VC++ 确实有/LTCG
用于链接时代码生成,这意味着它可能会在那里实现优化。
可以,但不能。可能这种情况不会经常发生,不足以证明可靠地优化此类调用所需的资源。
@j_random_hacker:要在链接时使用 /LTCG,必须首先使用 /GL 编译模块。如果您从其他人那里编译了模块,那么他们不太可能使用 /GL 编译。
我自己很好奇并启用了配置文件引导优化 (/LTCG:PGOptimize),这导致我的小程序在链接后重新优化。我启用了所有可能的速度优化,还启用了汇编程序列表 (/FAs)。在生成的汇编程序中,我看到对 break 的调用仍然是通过 Virtual Table ; 22 : p->Break(); mov edx, DWORD PTR [eax] mov ecx, eax mov eax, DWORD PTR [edx] call eax
完成的,而我添加到 CarImpl 的另一个非虚拟函数实际上被内联以代替它的调用。
@Denis:感谢您的测试。你测试的代码到底是什么?我想知道编译器是否可以证明它在编译时知道对象的动态类型(例如 ICar *p = new CarImpl; p->Break();
或不(如果声明为返回 ICar*
的函数在不同的翻译单元中被调用)定义,并且这个函数实际上返回一个指向CarImpl
对象的点)。以上是关于Visual C++ 中的去虚拟化的主要内容,如果未能解决你的问题,请参考以下文章
使用 C++ 在 Visual Studio 中创建虚拟绘图板 [关闭]
Visual Studio Express 中 C++ 中的内存分配问题