在C ++中使用接口会降低性能吗?

时间:2020-03-06 14:31:47  来源:igfitidea点击:

在C ++中使用接口(抽象基类)时,运行时性能会受到损失吗?

解决方案

与常规调用相比,每个虚拟函数调用的罚款很小。除非我们每秒进行数十万次调用,否则我们不太可能观察到差异,而且无论如何要为增加代码清晰度而付出的代价通常是值得的。

在C ++中使用抽象基类通常要求使用虚函数表,所有接口调用都将通过该表进行查找。与原始函数调用相比,此开销很小,因此请确保在担心之前比以前快得多。

我知道的唯一主要区别是,由于我们没有使用具体的类,所以内联很难(很多吗?)。

简短答案:不可以。

长答案:
不是基类,也不是类在其层次结构中具有的祖先数量会影响其速度。唯一的事情就是方法调用的成本。

非虚拟方法调用会产生成本(但可以内联)
虚拟方法调用的成本略高,因为我们需要在调用方法之前先查找要调用的方法(但这是一个简单的表查找,而不是搜索操作)。由于定义上接口上的所有方法都是虚拟的,因此会产生此成本。

除非我们正在编写一些对超速敏感的应用程序,否则这应该不是问题。使用界面会给我们带来额外的清晰度,通常可以弥补速度下降带来的影响。

我唯一能想到的是,虚拟方法的调用要比非虚拟方法慢一些,因为调用必须通过虚拟方法表进行。

但是,这是破坏设计的一个不好的理由。如果需要更高的性能,请使用速度更快的服务器。

当我们调用虚拟函数(例如通过接口)时,程序必须在表中查找该函数,以查看要为该对象调用的函数。与直接调用该函数相比,这样做的代价很小。

同样,当我们使用虚拟函数时,编译器无法内联函数调用。因此,将虚拟功能用于某些小功能可能会受到惩罚。通常,这是我们可能会看到的最大性能"命中"。如果函数很小且多次调用(例如从循环内部),则这实际上仅是一个问题。

对于任何包含虚函数的类,都使用vtable。显然,通过诸如vtable之类的调度机制调用方法比直接调用要慢,但是在大多数情况下,我们可以接受这种方法。

是的,但据我所知没有什么值得注意的。性能下降的原因是我们在每个方法调用中都具有"间接"功能。

但是,这实际上取决于我们使用的编译器,因为某些编译器无法在从抽象基类继承的类中内联方法调用。

如果我们想确定应该运行自己的测试。

应当注意的一件事是,虚拟函数调用成本可能因平台而异。在控制台上,它们可能更引人注目,因为通常,vtable调用表示高速缓存未命中,并且可能会使分支预测变差。

未内联使用虚拟调度调用的函数

对虚拟函数有一种惩罚,它很容易被遗忘:在对象类型不知道编译时间的(常见)情况下,虚拟调用不会被内联。如果函数很小并且适合于内联,则此代价可能非常重要,因为这不仅增加了调用开销,而且编译器在优化调用函数的方式上也受到限制(它必须假定虚函数可能已更改了某些寄存器或者内存位置,则无法在调用方和被调用方之间传播常量值)。

虚拟通话费用取决于平台

至于与正常函数调用相比的调用开销损失,答案取决于目标平台。如果目标计算机是x86 / x64 CPU,则调用虚拟函数的代价非常小,因为现代x86 / x64 CPU可以对间接调用执行分支预测。但是,如果我们以PowerPC或者其他RISC平台为目标,则虚拟呼叫损失可能会非常大,因为在某些平台上永远不会预测到间接呼叫(参见PC / Xbox 360跨平台开发最佳实践)。

我不认为成本比较是在虚拟函数调用和直接函数调用之间进行的。如果我们正在考虑使用抽象基类(接口),那么我们可能会想根据对象的动态类型执行几种操作之一。我们必须以某种方式做出选择。一种选择是使用虚拟功能。另一个是通过RTTI(可能很昂贵)或者在基类中添加type()方法(可能增加每个对象的内存使用量)来切换对象的类型。因此,应该将虚拟函数调用的成本与替代方法的成本进行比较,而不是与无所事事的成本进行比较。

在某些情况下适用的另一种替代方法是编译时
模板的多态性。例如,当我们需要
在程序开始时做出实现选择,并且
然后在执行期间使用它。一个例子
运行时多态

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>();
}

是的,有处罚。可以提高平台性能的是使用没有虚拟功能的非抽象类。然后使用成员函数指针指向非虚拟函数。

大多数人都注意到运行时的损失,这是正确的。

但是,以我在大型项目中的工作经验来看,清晰的界面和适当的封装所带来的好处很快就抵消了速度的提高。可以交换模块化代码以实现改进的实现,因此最终结果是很大的收益。

里程可能会有所不同,并且显然取决于我们正在开发的应用程序。

注意,多重继承通过多个vtable指针使对象实例膨胀。在x86上使用G ++,如果类具有虚拟方法而没有基类,则我们只有一个指向vtable的指针。如果我们有一个带有虚拟方法的基类,则仍然有一个指向vtable的指针。如果我们有两个带有虚方法的基类,则每个实例上都有两个vtable指针。

因此,通过多重继承(这是C ++中实现接口的方式),我们需要支付基类乘以对象实例大小中的指针大小。内存占用量的增加可能会间接影响性能。

我知道这是一个不常见的观点,但是即使提到此问题,我也怀疑我们在类结构中花了太多心思。我已经看到许多系统具有太多的"抽象级别",并且仅凭这些系统就容易导致严重的性能问题,这不是由于方法调用的成本,而是由于进行不必要的调用的趋势。如果这种情况发生在多个层面上,那将是杀手.。看一看