在多态基类上调用 C++ 虚函数是否与调用 C 样式函数指针一样快?真的有什么区别吗?
我正在考虑重构一些利用函数指针并将它们转换为多态中的虚函数的性能代码。
我想说大多数 C++ 实现的工作方式与此类似(可能是第一个编译成 C 的实现,生成的代码是这样的):
struct ClassVTABLE {
void (* virtuamethod1)(Class *this);
void (* virtuamethod2)(Class *this, int arg);
};
struct Class {
ClassVTABLE *vtable;
};
然后,给定一个实例Class x
,调用virtualmethod1
它的方法就像x.vtable->virtualmethod1(&x)
,因此有一个额外的取消引用,来自 的 1 个索引查找vtable
,以及一个额外的参数 (= this
) 压入堆栈/传入寄存器。
但是编译器可能可以优化函数内对实例的重复方法调用:由于实例Class x
在构造后无法更改其类,因此编译器可以将整体x.vtable->virtualmethod1
视为公共子表达式,并将其移出循环。因此,在这种情况下,在单个函数中重复的虚方法调用在速度上等同于通过简单的函数指针调用函数。
在多态基类上调用 C++ 虚函数是否与调用 C 样式函数指针一样快?真的有什么区别吗?
苹果和橙子。在极小的“一对一”级别上,虚函数调用涉及更多的工作,因为从进入vptr
到vtable
入口有间接/索引开销。
但是虚函数调用可以更快
好吧,这怎么可能?我刚才说虚函数调用需要更多的工作,这是真的。
人们往往忘记的是在这里尝试进行更仔细的比较(尝试使其少一点苹果和橙子,即使它是苹果和橙子)。我们通常不会创建一个只有一个虚函数的类。如果我们这样做了,那么性能(甚至像代码大小这样的东西)肯定会偏爱函数指针。我们经常有更多这样的东西:
class Foo
{
public:
virtual ~Foo() {}
virtual f1() = 0;
virtual f2() = 0;
virtual f3() = 0;
virtual f4() = 0;
};
...在这种情况下,更“直接”的函数指针类比可能是:
struct Bar
{
void (*f1)();
void (*f2)();
void (*f3)();
void (*f4)();
};
在这种情况下,在 的每个实例中调用虚函数Foo
可能比Bar
. 这是因为Foo
只需要将一个 vptr 存储到一个被重复访问的中央 vtable 中。有了这个,我们得到了改进的引用局部性(更小Foos
并且可以更好地适应缓存行的数量,更频繁地访问Foo's
中央 vtable)。
Bar
另一方面,它需要更多内存,并且Foo's
在每个实例中有效地复制了 vtable的内容Bar
(假设有一百万个Foo
and实例Bar
)。在这种情况下,膨胀大小的冗余数据量Bar
通常会大大超过每次函数指针调用做的工作量稍少的成本。
如果我们只需要为每个对象存储一个函数指针,并且这是一个极端热点,那么只存储一个函数指针可能会更好(例如:对于远程实现任何类似于std::function
只存储一个函数指针的人来说,这可能很有用)。
所以它有点像苹果和橘子,但如果我们正在对一个与此类似的用例进行建模,那么存储一个中央共享函数地址表(在 C 或 C++ 中)的 vtable 类型的方法可以大大提高效率。
如果我们正在建模一个用例,其中我们只有一个函数指针存储在一个对象中,而一个 vtable 中只有一个虚函数,那么函数指针的效率会稍微高一些。
不太可能你会看到很大的差异,但就像所有这些事情一样,通常是小细节(例如编译器需要将this
指针传递给虚函数)会导致性能差异。函数本身是一个“在后台”的virtual
函数指针,所以一旦编译器完成了它的工作,在这两种情况下你可能会得到非常相似的代码。
这听起来像是很好地利用了虚函数,如果有人反对并说“会有性能差异”,我会说“证明它”。但是,如果您想避免进行该讨论,请制定一个基准(如果还没有)来衡量现有代码的性能,重构它(或其中的一部分)并比较结果。理想情况下,在几台不同的机器上进行测试,这样您就不会得到在您的机器上运行得更好的结果,但在某些其他类型的机器(不同代的处理器、不同的制造商或处理器等)上却不是那么好。
虚函数调用涉及两个取消引用,其中一个是索引的,即类似*(object->_vtable[3])()
.
通过函数指针的调用涉及一个取消引用。
方法调用还需要传递隐藏参数以作为this
.
除非方法体实际上是空的并且没有参数或返回值,否则您不太可能注意到差异。
函数指针调用和虚函数调用之间的差异可以忽略不计,除非您已经测量到上述是瓶颈。
唯一的区别是:
这是因为虚函数需要查找要调用的函数的地址,而函数指针已经知道它(因为它存储在自身中)。
我要补充一点,因为您正在使用 C++,所以虚拟方法应该是要走的路。