如果将对象强制转换为实际类型,是否需要使用虚函数?

时间:2015-07-10 10:26:48

标签: c++ performance inheritance vtable

我的理解是,由于两个问题,虚函数会导致性能问题:vtable引起的额外derefencing以及编译器无法在多态代码中内联函数。

如果我将变量指针向下转换为其确切类型怎么办?那么还有额外的费用吗?

class Base { virtual void foo() = 0; };
class Derived : public Base { void foo() { /* code */} };

int main() {
    Base * pbase = new Derived();
    pbase->foo(); // Can't inline this and have to go through vtable
    Derived * pderived = dynamic_cast<Derived *>(pbase);
    pderived->foo(); // Are there any costs due to the virtual method here?
}

我的直觉告诉我,由于我将对象转换为其实际类型,编译器应该能够避免使用虚函数的缺点(例如,它应该能够内联方法调用,如果它想要)。这是对的吗?

编译器实际上可以知道pderived在我转发之后是Derived类型吗?在上面的例子中,看到pbase是Derived类型的微不足道,但在实际代码中它可能在编译时是未知的。

既然我已经写下来了,我想由于Derived类本身可以被另一个类继承,所以将pbase向下转换为Derived指针实际上并不能确保编译器的任何内容,因此它无法避免拥有虚拟功能的成本?

3 个答案:

答案 0 :(得分:21)

神话Sufficiently Smart Compiler能做什么,以及实际的编译器最终会做什么之间总是存在差距。在您的示例中,由于没有从Derived继承的内容,最新的编译器可能会将调用虚拟化为foo。但是,由于成功的虚拟化和后续内联通常是一个难题,因此请尽可能使用final关键字帮助编译器。

class Derived : public Base { void foo() final { /* code */} }

现在,编译器知道foo只能调用一个Derived* {。}}。

(有关为什么虚拟化很难以及gcc4.9 +如何处理它的深入讨论,请阅读Jan Hubicka的Devirtualization in C++系列文章。)

答案 1 :(得分:5)

Pradhan使用final的建议是合理的,如果更改Derived类是您的选项,并且您不希望任何进一步的推导。

特定呼叫站点可直接使用的另一个选项是在函数名前加上Derived::,禁止虚拟调度到任何进一步的覆盖:

#include <iostream>

struct Base { virtual ~Base() { } virtual void foo() = 0; };

struct Derived : public Base
{
    void foo() override { std::cout << "Derived\n"; }
};

struct FurtherDerived : public Derived
{
    void foo() override { std::cout << "FurtherDerived\n"; }
};

int main()
{
    Base* pbase = new FurtherDerived();
    pbase->foo(); // Can't inline this and have to go through vtable
    if (Derived* pderived = dynamic_cast<Derived *>(pbase))
    {
        pderived->foo();  // still dispatched to FurtherDerived
        pderived->Derived::foo();  // static dispatch to Derived
    }
}

输出:

FurtherDerived
FurtherDerived
Derived

这可能很危险:实际的运行时类型可能取决于调用它的覆盖以维持其不变量,因此除非存在紧迫的性能问题,否则使用它是个坏主意。

可用代码here

答案 2 :(得分:3)

实际上,去虚拟化是一种非常特殊的常量传播情况,其中传播的常量是类型(一般物理上表示为v-ptr,但标准不能保证)。

完全虚拟化

在多种情况下,编译器实际上可以虚拟化您可能没有想到的调用:

int main() {
    Base* base = new Derived();
    base->foo();
}

Clang能够在上面的例子中虚拟化调用,因为它可以跟踪在范围内创建的base的实际类型。

以类似的方式:

struct Base { virtual void foo() = 0; };
struct Derived: Base { virtual void foo() override {} };

Base* create() { return new Derived(); }

int main() {
    Base* base = create();
    base->foo();
}

虽然这个例子稍微复杂一点,并且Clang前端不会意识到base必然是类型Derived,后来出现的LLVM优化器将会:

  • create
  • 中的内联main
  • 存储指向Derived
  • base->vptr的v表的指针
  • 意识到base->foo()因此是base->Derived::foo()(通过v-ptr解析间接)
  • 并最终优化了所有内容,因为Derived::foo
  • 无关

这是最终结果(我认为即使是那些未启动LLVM IR的人也不需要评论):

define i32 @main() #0 {
  ret i32 0
}

有多个实例,编译器(前端或后端)可以在可能不明显的情况下对调用进行虚拟化,在所有情况下,它归结为它能够证明对象的运行时类型指着。

部分虚拟化

在他关于devirutalization主题的gcc编译器的改进中,JanHubička引入了部分虚拟化。

gcc的最新版本能够快速列出对象的一些可能的运行时类型,特别是产生以下伪代码(在这种情况下,两个被认为是可能的,并非所有都是已知的或可能足以证明特殊情况的合理性):

// Source
void doit(Base* base) { base->foo(); }

// Optimized
void doit(Base* base) {
    if (base->vptr == &Derived::VTable) { base->Derived::foo(); }
    else if (base->ptr == &Other::VTable) { base->Other::foo(); }
    else {
        (*base->vptr[Base::VTable::FooIndex])(base);
    }
}

虽然这可能看起来有点复杂,但如果预测正确,它确实可以提供一些性能提升(正如您从文章系列中看到的那样)。

似乎令人惊讶?好吧,还有更多测试,但base->Derived::foo()base->Other::foo()现在可以内联,这本身就可以开辟更多优化机会:

  • 在这种特殊情况下,由于Derived::foo()什么都不做,函数调用可以被优化掉; if测试的惩罚小于函数调用的惩罚,所以如果条件经常匹配则值得。
  • 在其中一个函数参数已知或已知具有某些特定属性的情况下,后续的常量传播过程可以简化函数的内联体

令人印象深刻,对吧?

好吧,好吧,这是相当啰嗦但我来讨论dynamic_cast<Derived*>(base)

首先,dynamic_cast的成本不容小觑;实际上,它可能比开始使用base->foo()更加昂贵,你已经被警告了。

其次,使用dynamic_cast<Derived*>(base)->foo()确实可以允许对函数调用进行虚拟化,如果它为编译器提供了足够的信息(至少它会提供更多信息)。通常,这可以是:

  • 因为Derived::foofinal
  • 因为Derivedfinal
  • 因为Derived是在匿名命名空间中定义的,并且没有后代重新定义foo,因此只能在此翻译单元(大致为.cpp文件)中访问,因此所有后代都是已知且可以检查
  • 还有很多其他案例(比如在部分虚拟化的情况下修剪一组潜在的候选人)

如果你真的希望确保虚拟化,那么final应用于函数或类是最好的选择。