在多态基类上调用C ++虚函数和调用C风格函数指针一样快吗?真的有什么不同吗?
我正在考虑重构一些具有性能意识的代码,这些代码利用函数指针并将其更改为多态中的虚函数。
答案 0 :(得分:24)
我会说大多数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)
,因此需要一个额外的解引用,一个来自vtable
的索引查找,一个额外的参数(= this
)被推入堆栈/传入寄存器。
然而,编译器可能可以优化函数内实例上的重复方法调用:由于实例Class x
在构造之后无法更改其类,因此编译器可以将整个x.vtable->virtualmethod1
视为常见子表达式,并将其移出循环。因此,在这种情况下,在单个函数中重复的虚方法调用在速度上等同于通过简单的函数指针调用函数。
答案 1 :(得分:11)
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
的每个实例中有效地复制Bar
vtable的内容(假设有一百万个Foo
实例1}}和Bar
)。在这种情况下,膨胀Bar
大小的冗余数据量通常会大大超过每个函数指针调用稍微减少工作量的成本。
如果我们只需要为每个对象存储一个函数指针,这是一个极端的热点,那么只存储一个函数指针可能会很好(例如:它可能对某些实现远程类似{{1只存储一个函数指针。)
所以它是苹果和橘子,但如果我们正在建模一个与此接近的用例,那么存储一个中央共享函数地址表(用C或C ++)的vtable方法可能会更多高效。
如果我们正在建模一个用例,其中我们只有一个存储在一个对象中的单个函数指针而另一个只有一个虚函数的vtable,那么函数指针的效率会略高一些。
答案 2 :(得分:6)
不可思议的是,你会看到很多不同之处,但是就像所有这些事情一样,通常是小细节(例如编译器需要将this
指针传递给虚函数)会导致差异在表现。 virtual
函数本身就是一个“引擎盖下”的函数指针,所以一旦编译器完成它,你可能会在两种情况下得到非常相似的代码。
这听起来好像是虚拟功能的使用,如果有人反对并说“会有性能差异”,我会说“证明它”。但是如果你想避免讨论,那么制定一个基准测试(如果还没有测试),测量现有代码的性能,重构它(或它的某些部分)并比较结果。理想情况下,在几台不同的机器上进行测试,这样您就不会在您的机器上获得更好的效果,但在其他类型的机器(不同代的处理器,不同的制造商或处理器等)上却不是那么好。
答案 3 :(得分:5)
虚函数调用涉及两个解引用,其中一个是索引的,即类似*(object->_vtable[3])()
。
通过函数指针调用涉及一个解除引用。
方法调用还要求将隐藏的参数传递给this
。
除非方法体实际上是空的并且没有参数或返回值,否则您最不可能注意到差异。
答案 4 :(得分:2)
除非你已经测量过以上是瓶颈,否则函数指针调用和虚函数调用之间的区别可以忽略不计。
唯一的区别是:
这是因为虚函数需要查找它将要调用的函数的地址,而函数指针已经知道它(因为它存储在自身中)。
我想补充一点,因为你正在使用C ++,虚拟方法应该是最佳选择。