在C ++中使用接口(抽象基类)时是否存在运行时性能损失?
答案 0 :(得分:45)
简答:不。
长答案: 它不是基类或类在其层次结构中具有影响其速度的祖先数。唯一的问题是方法调用的成本。
非虚拟方法调用具有成本(但可以内联)
虚拟方法调用的成本略高,因为您需要在调用之前查找要调用的方法(但这是一个简单的表查找不搜索)。由于接口上的所有方法都是虚拟的,因此存在这种成本。
除非您正在编写一些超高速敏感应用程序,否则这应该不是问题。通过界面获得的额外清晰度通常可以弥补任何感知速度的降低。
答案 1 :(得分:23)
虚拟函数存在一种容易被遗忘的惩罚:虚拟调用不会在对象类型不知道编译时间的(常见)情况下内联。如果你的函数很小并且适合内联,那么这个代价可能非常大,因为你不仅增加了调用开销,而且编译器也限制了它如何优化调用函数(它必须假设虚函数可能已经更改了一些寄存器或内存位置,它不能在调用者和被调用者之间传播常量值。
对于与正常函数调用相比的调用开销惩罚,答案取决于您的目标平台。如果您的目标是使用x86 / x64 CPU的PC,则调用虚拟功能的代价非常小,因为现代x86 / x64 CPU可以对间接调用执行分支预测。但是,如果您的目标是PowerPC或其他一些RISC平台,则虚拟调用惩罚可能非常大,因为在某些平台上从不预测间接调用(参见PC/Xbox 360 Cross Platform Development Best Practices)。
答案 2 :(得分:9)
与常规呼叫相比,每个虚拟功能呼叫都有一个小的惩罚。除非您每秒进行数十万次通话,否则您不太可能发现差异,而且价格通常值得支付以增加代码清晰度。
答案 3 :(得分:5)
当你调用一个虚函数(比如通过一个接口)时,程序必须在表中查找函数,看看为该对象调用哪个函数。与直接调用函数相比,这会带来一点点损失。
此外,当您使用虚函数时,编译器无法内联函数调用。因此,对于某些小功能使用虚函数可能会受到惩罚。这通常是您可能会看到的最大“性能”。如果函数很小并且多次调用(例如在循环内),这实际上只是一个问题。
答案 4 :(得分:4)
在某些情况下适用的另一种替代方法是编译时 多态与模板。例如,当您需要时,它很有用 在程序开始时做出实现选择,并且 然后在执行期间使用它。一个例子 运行时多态性
class AbstractAlgo
{
virtual int func();
};
class Algo1 : public AbstractAlgo
{
virtual int func();
};
class Algo2 : public AbstractAlgo
{
virtual int func();
};
void compute(AbstractAlgo* algo)
{
// Use algo many times, paying virtual function cost each time
}
int main()
{
int which;
AbstractAlgo* algo;
// read which from config file
if (which == 1)
algo = new Algo1();
else
algo = new Algo2();
compute(algo);
}
使用编译时多态性
class Algo1
{
int func();
};
class Algo2
{
int func();
};
template<class ALGO> void compute()
{
ALGO algo;
// Use algo many times. No virtual function cost, and func() may be inlined.
}
int main()
{
int which;
// read which from config file
if (which == 1)
compute<Algo1>();
else
compute<Algo2>();
}
答案 5 :(得分:3)
我不认为成本比较是在虚函数调用和直接函数调用之间。如果您正在考虑使用抽象基类(接口),那么您可能希望根据对象的动态类型执行多个操作之一。你必须以某种方式作出这种选择。一种选择是使用虚函数。另一种是通过RTTI(可能很昂贵)或者将类型()方法添加到基类(可能增加每个对象的内存使用)来切换对象的类型。因此,虚拟函数调用的成本应该与替代成本进行比较,而不是无所事事的成本。
答案 6 :(得分:3)
大多数人都会注意到运行时的惩罚,这是正确的。
然而,根据我从事大型项目的经验,清晰的接口和适当的封装带来的好处很快抵消了速度的提高。可以交换模块化代码以实现改进的实现,因此净结果是一个很大的增益。
您的里程可能会有所不同,这显然取决于您正在开发的应用程序。
答案 7 :(得分:2)
应该注意的一点是虚拟函数调用成本可能因平台而异。在控制台上,它们可能更明显,因为通常vtable调用意味着缓存未命中并且可能会导致分支预测。
答案 8 :(得分:2)
请注意,多重继承会使用多个vtable指针使对象实例膨胀。使用x86上的G ++,如果你的类有一个虚方法而没有基类,你有一个指向vtable的指针。如果你有一个带有虚方法的基类,你仍然有一个指向vtable的指针。如果您有两个带有虚方法的基类,则每个实例上都有两个 vtable指针 。
因此,使用多重继承(这是在C ++中实现接口的原因),您需要在对象实例大小中支付基类时间指针大小。内存占用的增加可能会产生间接的性能影响。
答案 9 :(得分:0)
在C ++中使用抽象基类通常要求使用虚函数表,所有的接口调用都将通过该表进行查找。与原始函数调用相比,成本很小,因此请确保在担心之前需要比这更快。
答案 10 :(得分:0)
我所知道的唯一主要区别是,由于你没有使用具体的类,内联(更多?)更难做。
答案 11 :(得分:0)
我唯一能想到的是,虚拟方法的调用速度比非虚拟方法要慢一些,因为调用必须经过virtual method table。
然而,这是搞砸你的设计的一个坏理由。如果您需要更高的性能,请使用更快的服务器。
答案 12 :(得分:0)
对于包含虚函数的任何类,使用vtable。显然,通过像vtable这样的调度机制调用方法比直接调用要慢,但在大多数情况下,你可以使用它。
答案 13 :(得分:0)
是的,但据我所知,没什么值得注意的。性能上升是因为每次方法调用都有“间接”。
但是,它实际上取决于您正在使用的编译器,因为某些编译器无法在继承自抽象基类的类中内联方法调用。
如果你想确定你应该自己进行测试。
答案 14 :(得分:0)
是的,有罚款。可以提高平台性能的一点是使用没有虚函数的非抽象类。然后使用指向非虚函数的成员函数指针。
答案 15 :(得分:0)
我知道这是一个不寻常的观点,但即便提到这个问题也让我怀疑你在课堂结构上花了太多心思。我已经看到很多系统有太多“抽象级别”,而这一点使它们容易出现严重的性能问题,不是由于方法调用的成本,而是由于倾向于进行不必要的调用。如果这发生在多个层面,那就是杀手锏。 take a look