【发布时间】:2013-02-02 02:12:27
【问题描述】:
我有一个纯抽象基类和两个派生类:
struct B { virtual void foo() = 0; };
struct D1 : B { void foo() override { cout << "D1::foo()" << endl; } };
struct D2 : B { void foo() override { cout << "D1::foo()" << endl; } };
在 A 点调用 foo 是否与调用非虚拟成员函数的成本相同?或者它是否比 D1 和 D2 不是从 B 派生的更昂贵?
int main() {
D1 d1; D2 d2;
std::vector<B*> v = { &d1, &d2 };
d1.foo(); d2.foo(); // Point A (polymorphism not necessary)
for(auto&& i : v) i->foo(); // Polymorphism necessary.
return 0;
}
答案: Andy Prowl 的答案是正确的答案,我只是想添加 gcc 的汇编输出(在 godbolt 中测试:gcc- 4.7 -O2 -march=native -std=c++11)。直接函数调用的成本是:
mov rdi, rsp
call D1::foo()
mov rdi, rbp
call D2::foo()
对于多态调用:
mov rdi, QWORD PTR [rbx]
mov rax, QWORD PTR [rdi]
call [QWORD PTR [rax]]
mov rdi, QWORD PTR [rbx+8]
mov rax, QWORD PTR [rdi]
call [QWORD PTR [rax]]
但是,如果对象不是从B 派生的,而您只是执行直接调用,gcc 将内联函数调用:
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
如果D1 和D2 不是从B 派生的,这可以进一步优化,所以我猜不,它们不等效(在至少对于具有这些优化的这个版本的 gcc,-O3 在没有内联的情况下产生了类似的输出)。在D1 和D2 确实派生自B 的情况下,是否存在阻止编译器内联的东西?
“修复”:使用委托(也就是自己重新实现虚函数):
struct DG { // Delegate
std::function<void(void)> foo;
template<class C> DG(C&& c) { foo = [&](void){c.foo();}; }
};
然后创建一个代表向量:
std::vector<DG> v = { d1, d2 };
如果您以非多态方式访问方法,这允许内联。但是,我猜访问向量会比只使用虚函数(还不能用 Godbolt 测试)要慢(或者至少一样快,因为std::function 使用虚函数进行类型擦除)。
【问题讨论】:
-
如果
D1和D2是从B派生的直接调用,编译器没有理由不能内联调用。 -
你无法计算这些指令集的差异。
-
没有什么能阻止编译器内联
D1::foo()、D2::foo()。这是一些GCC 4.7及以上的故障。GCC 4.5内联这个没有问题。clang 3.4.1也内联了这个。 -
它仍然因 gcc-4.9 (tip-of-trunk) -O3 -march=native -DNDEBUG 而失败(请参阅此处的代码和程序集:goo.gl/NKm3Uz)。它应该内联这些调用,因为我们只有一个 TU。在更复杂的程序中,除非您使用
final,否则即使使用 LTO 也很难内联这些,因为您始终可以创建一个新的 TU,您可以在其中从一个类派生(动态库也可以这样做) . IIRC Herb Sutter 将这个问题描述为“通过虚拟继承,你需要为无限的可扩展性付出代价”,这是有代价的。 -
此外,通过虚拟继承,接口(或所有可能的接口,除非您使用适配器模式)与对象一起放入 vtable 中,并且此 vtable 可以变得很大。委托提供了更小的接口(和 vtable),这提高了循环中的缓存使用率。
标签: c++ performance virtual-functions