我的理解是,由于两个问题,虚函数会导致性能问题: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指针实际上并不能确保编译器的任何内容,因此它无法避免拥有虚拟功能的成本?
答案 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::foo
是final
Derived
是final
Derived
是在匿名命名空间中定义的,并且没有后代重新定义foo
,因此只能在此翻译单元(大致为.cpp
文件)中访问,因此所有后代都是已知且可以检查如果你真的希望确保虚拟化,那么final
应用于函数或类是最好的选择。