确定性时的虚函数开销 (C++)
virtual function overhead when deterministic (c++)
我知道虚函数本质上是包含在 vtable 上的函数指针,这使得多态调用由于间接等原因而变慢。但是我想知道当调用是确定性的时编译器优化。确定性是指以下情况:
- 对象是值而不是引用,因此不可能有多态性:
struct Foo
{
virtual void DoSomething(){....}
};
int main()
{
Foo myfoo;
myfoo.DoSemthing();
return 0;
}
- 参考是一个无子女的班级:
struct Foo
{
virtual void DoSomething();
};
struct Bar : public Foo
{
virtual void DoSomething();
};
int main()
{
Foo* a = new Foo();
a->DoSomething(); //Overhead ? a doesn't seem to be able to change nature.
Foo* b = new Bar();
b->DoSomething(); //Overhead ? It's a polymorphic call, but b's nature is deterministic.
Bar* c = new Bar();
c->DoSomething(); //Overhead ? It is NOT possible to have an other version of the method than Bar::DoSomething
return 0;
}
在第一种情况下,这将不是虚拟调用。编译器将直接向Foo::DoSomething()
发出调用。
在第二种情况下,它更复杂。首先,它充其量是一个链接时间优化,因为对于特定的翻译单元,编译器不知道还有谁可能从该类继承。您遇到的另一个问题是共享库,它也可能在您的可执行文件不知道的情况下继承。
不过,一般来说,这是一种编译器优化,称为虚函数调用消除或去虚拟化,并且在某种程度上是一个活跃的研究领域。有些编译器在某种程度上这样做,有些则根本不这样做。
请参阅,在 GCC (g++( 中,-fdevirtualize
和 -fdevirtualize-speculatively
。这些名称暗示了保证的质量水平。
在 Visual Studio 2013 中,即使行为是确定性的,也不会优化虚拟函数调用。
例如
#include <iostream>
static int counter = 0;
struct Foo
{
virtual void VirtualCall() { ++counter; }
void RegularCall() { ++counter; }
};
int main()
{
Foo* a = new Foo();
a->VirtualCall(); //Overhead ? a doesn't seem to be able to change nature.
a->RegularCall();
std::cout << counter;
return 0;
}
虚拟呼叫的机器代码如下所示:
a->VirtualCall()
0001b 8b 01 mov eax, DWORD PTR [ecx]
0001d ff 10 call DWORD PTR [eax]
常规调用的机器代码显示函数是内联的 - 没有函数调用:
a->RegularCall()
00 inc DWORD PTR _counter
一般来说,你可以相信你的编译器优化器会做出好的选择,当然这取决于优化设置。
为了验证概念,这里有一个使用不同情况的代码,Foo
和Bar
被定义为你所做的:
struct Tzar : public Foo
{
void DoSomething() override final; // this is a virtual than can't be overriden further
};
Foo* factory ();
Bar* bar_factory();
Tzar* tsar_factory();
int main()
{
Foo myfoo;
myfoo.DoSomething(); // this is a direct call
Foo* a = new Foo();
a->DoSomething(); //Overhead only without optimisation: a is clearly a Foo, so Foo::DoSomething().
Foo* b = new Bar();
b->DoSomething(); //Overhead only without optimisation: b is clearly a Bar, so Bar::DoSomething().
Bar* c = new Bar();
c->DoSomething(); //Overhead only without optimisation: c is clearly a Bar, so Bar::DoSomething
Foo* d = factory();
d->DoSomething(); // Overhead required: we don't know the type of d, unless global optimisation could predict it
a = d;
a->DoSomething(); //the unknown propagates to a, so now this call is indirect
Foo*e = bar_factory();
e->DoSomething(); // Overhead required: we don't know the type of e: could be a Bar or a furhter derivate unknown in this compilation unit
Foo*f = tsar_factory();
f->DoSomething(); // Overhead could be optimised away : we don't know the type of f, but f::DoSomething() can't be overriden further
// but currently it isn't
return 0;
}
您可以在此处找到为您使用 GCC 5.3.0 提交的所有案例生成的汇编代码,而无需优化。 它的颜色可帮助您查看每个C++语句的汇编代码。
第一个调用将始终是直接调用:
lea rax, [rbp-80] ; take the object pointer from the stack
mov rdi, rax ; set the this pointer of the invoking object
call Foo::DoSomething() ; direct call to the function
如果不进行优化,DoSomething()
的所有其他调用都将使用间接调用。 这里是b->DoSomething()
的例子:
mov rax, QWORD PTR [rbp-32]
mov rax, QWORD PTR [rax]
mov rax, QWORD PTR [rax] ; load the function call from the vtable
mov rdx, QWORD PTR [rbp-32]
mov rdi, rax ; set the this pointer of the invoking object
call rax ; indirect call via register
如果现在在编译器选项中设置优化标志 -O2,则会看到当编译器可以预测多态指针的实际类型时,大多数间接调用都会被优化掉。 在上面的示例中,它将是:
mov rdi, rax ; set the this pointer of the invoking object
call Bar::DoSomething() ; direct call !!
当编译器无法安全地预测实际类型时,它将使用间接调用。 例如,如果你有一个函数 bar_factory()
,它返回一个Bar
指针,编译器无法知道它是返回指向 Bar
对象的指针,还是返回指向派生自 Bar
的类的对象(可以在另一个编译单元中定义,但此处不知道(。
唯一意想不到的一点是当你将一个虚函数定义为最终覆盖时(在我的示例中Tzar
类(。 在这里,您可以期望编译器将利用DoSomething()
不应该进一步派生的事实。 但这不一定完成。
- 实现无开销push_back的最佳方法是什么
- 别名模板的专业化 C++11 中没有开销的最佳替代方案
- C++标准是否允许<double>在没有开销的情况下实现 std::可选
- 类型擦除的std::function与虚拟函数调用的开销
- 一组值的零开销下标运算符
- C++ 特征库:引用的性能开销<>
- C++对开销较少的容器使用多个过滤器
- 在编译时评估函数开销的通用方法
- 在循环中调用同一虚函数的开销
- 使用静态成员函数而不是普通函数是否有任何开销?
- 自定义运算符重载C++,无开销
- 在 v8 JavaScript 中重复调用C++是否有巨大的开销?
- 将 mmap 内存用于开销非常低的循环缓冲区
- 与纯 V8 相比,NodeJS 是否有任何性能缺陷或显著开销?
- 非 constexpr 变量模板的开销是否为零?
- 右值引用是否具有与右值引用相同的开销?
- 实例成员与静态成员与非类方法的开销
- 如果使用lambda,std::unique_ptr如何没有大小开销
- 放置新[]的开销
- 这种获取模板参数包中最后一个元素的方法是否有隐藏的开销?