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 中创建虚拟绘图板 [关闭]

VC(Visual Studio C++)虚拟键VK值列表

Visual Studio Express 中 C++ 中的内存分配问题

Visual Studio:源代码控制中的Python虚拟环境

Xshell连接Visual box中的虚拟机的方法

PAI:智能虚拟化形象的去中心化平台 | ONETOP评级