在 C++ 中使用接口(抽象基类)时是否存在运行时性能损失?
16 回答
简短的回答:没有。
长答案:影响其速度的不是基类或类在其层次结构中的祖先数量。唯一的问题是方法调用的成本。
非虚拟方法调用有成本(但可以内联)
虚拟方法调用的成本略高,因为您需要在调用它之前查找要调用的方法(但这是一个简单的表查找而不是搜索) . 由于接口上的所有方法根据定义都是虚拟的,因此存在此成本。
除非您正在编写一些对超速敏感的应用程序,否则这应该不是问题。您从使用界面中获得的额外清晰度通常可以弥补任何感知到的速度下降。
使用虚拟调度调用的函数没有内联
对于虚函数有一种很容易忘记的惩罚:在对象类型不知道编译时间的(常见)情况下,虚调用不会内联。如果您的函数很小并且适合内联,那么这种损失可能非常显着,因为您不仅增加了调用开销,而且编译器也限制了它如何优化调用函数(它必须假设虚函数可能已经改变了一些寄存器或内存位置,它不能在调用者和被调用者之间传播常量值)。
虚拟通话费用取决于平台
至于与普通函数调用相比的调用开销损失,答案取决于您的目标平台。如果您的目标是具有 x86/x64 CPU 的 PC,调用虚拟函数的代价非常小,因为现代 x86/x64 CPU 可以对间接调用执行分支预测。但是,如果您的目标是 PowerPC 或其他 RISC 平台,则虚拟调用损失可能相当大,因为在某些平台上从未预测过间接调用(参见PC/Xbox 360 跨平台开发最佳实践)。
与常规调用相比,每个虚函数调用都会受到少量惩罚。除非您每秒执行数十万次调用,否则您不太可能观察到差异,而且无论如何,为增加代码清晰度付出代价通常是值得的。
当你调用一个虚函数(比如通过一个接口)时,程序必须在表中查找该函数,以查看为该对象调用哪个函数。与直接调用函数相比,这会带来很小的损失。
此外,当您使用虚函数时,编译器无法内联函数调用。因此,对一些小函数使用虚函数可能会受到惩罚。这通常是您可能看到的最大性能“打击”。如果函数很小并且被多次调用,这真的只是一个问题,比如在一个循环中。
在某些情况下适用的另一种替代方法是使用模板的编译时多态性。例如,当您想在程序开始时做出实现选择,然后在执行期间使用它时,它很有用。一个运行时多态的例子
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>();
}
我不认为虚拟函数调用和直接函数调用之间的成本比较。如果您正在考虑使用抽象基类(接口),那么您会遇到一种情况,即您希望根据对象的动态类型执行多个操作之一。你必须以某种方式做出选择。一种选择是使用虚函数。另一种是通过 RTTI(可能很昂贵)或向基类添加 type() 方法(可能会增加每个对象的内存使用)来切换对象的类型。所以虚函数调用的成本应该与替代的成本进行比较,而不是与什么都不做的成本进行比较。
大多数人都注意到运行时的惩罚,这是正确的。
然而,根据我从事大型项目的经验,清晰的接口和适当的封装所带来的好处很快就抵消了速度的提升。模块化代码可以交换为改进的实现,因此最终结果是巨大的收益。
您的里程可能会有所不同,这显然取决于您正在开发的应用程序。
请注意,多重继承使用多个 vtable 指针使对象实例膨胀。在 x86 上使用 G++,如果您的类有一个虚方法但没有基类,那么您有一个指向 vtable 的指针。如果你有一个带有虚方法的基类,你仍然有一个指向 vtable 的指针。如果您有两个带有虚方法的基类,则每个实例上都有两个vtable 指针。
因此,使用多重继承(这是在 C++ 中实现接口的方式),您需要支付基类乘以对象实例大小中的指针大小。内存占用的增加可能会对性能产生间接影响。
应该注意的一件事是,虚拟函数调用成本可能因平台而异。在控制台上,它们可能更明显,因为通常 vtable 调用意味着缓存未命中并且可能会破坏分支预测。
在 C++ 中使用抽象基类通常要求使用虚函数表,所有接口调用都将通过该表查找。与原始函数调用相比,成本很小,因此在担心它之前,请确保您需要比这更快。
我知道的唯一主要区别是,由于您没有使用具体的类,因此内联(很多?)更难做到。
我唯一能想到的是,调用虚方法比非虚方法慢一点,因为调用必须通过虚方法表。
然而,这是搞砸你的设计的一个不好的理由。如果您需要更高的性能,请使用更快的服务器。
对于任何包含虚函数的类,都使用 vtable。显然,通过像 vtable 这样的分派机制调用方法比直接调用要慢,但在大多数情况下,您可以接受。
是的,但据我所知没有什么值得注意的。性能下降是因为您在每个方法调用中都有“间接”。
但是,这实际上取决于您使用的编译器,因为某些编译器无法内联从抽象基类继承的类中的方法调用。
如果你想确定你应该运行自己的测试。
是的,有罚款。可以提高平台性能的方法是使用没有虚函数的非抽象类。然后使用指向您的非虚拟函数的成员函数指针。
我知道这是一个不常见的观点,但即使提到这个问题,我也会怀疑你对班级结构的思考太多了。我见过许多具有太多“抽象级别”的系统,仅此一项就使它们容易出现严重的性能问题,这不是由于方法调用的成本,而是由于倾向于进行不必要的调用。如果这种情况发生在多个层次上,那就是一个杀手。看一看