【问题标题】:Performance penalty for working with interfaces in C++?在 C++ 中使用接口的性能损失?
【发布时间】:2010-09-11 22:42:21
【问题描述】:

在 C++ 中使用接口(抽象基类)时是否存在运行时性能损失?

【问题讨论】:

  • this问题的答案也有关系。

标签: c++ performance abstract-class virtual-functions


【解决方案1】:

简短回答:不。

长答案: 影响其速度的不是基类或类在其层次结构中的祖先数量。唯一的问题是方法调用的成本。

非虚方法调用是有代价的(但可以内联)
虚拟方法调用的成本略高,因为您需要在调用它之前查找要调用的方法(但这是一个简单的表查找不是搜索)。由于接口上的所有方法根据定义都是虚拟的,因此存在此成本。

除非您正在编写一些对超速敏感的应用程序,否则这应该不是问题。您从使用界面获得的额外清晰度通常可以弥补任何感知到的速度下降。

【讨论】:

  • 在具有多个基类的对象上调用虚方法的成本再次比在具有单个继承层次结构的对象上调用虚方法的成本略高,这一点毫无价值。
  • 你确定吗?你有这个评论的来源吗?
  • 成本在转化中。如果您有 'D*' 并且想要转换为 'B2*',那么,如果类的布局是 [B1, B2],编译器需要返回 'D* + offset to B2'。我不同意这是值得注意的 - 这将是微不足道的。
  • 甚至可能为零,具体取决于您的 CPU 提供的寻址模式。访问寄存器+偏移量并不稀奇。
  • @Martin:实际上,在非虚拟方法存在的情况下,虚拟方法将被内联——即,当对象的动态类型可以在编译时确定时,方法是可内联的(不是太大 + 在类定义中或使用“内联”声明)。
【解决方案2】:

使用虚拟调度调用的函数没有内联

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

虚拟通话费用取决于平台

至于与普通函数调用相比的调用开销损失,答案取决于您的目标平台。如果您的目标是具有 x86/x64 CPU 的 PC,调用虚拟函数的代价非常小,因为现代 x86/x64 CPU 可以对间接调用执行分支预测。但是,如果您的目标是 PowerPC 或其他 RISC 平台,则虚拟调用损失可能相当大,因为在某些平台上永远无法预测间接调用(参见PC/Xbox 360 Cross Platform Development Best Practices)。

【讨论】:

  • 虚拟调用没有内联的说法是不正确的。只要编译器可以在编译时确定对象的最终类型,对该对象的方法调用就是内联的候选对象。只有在通过指向基址的指针调用时才不能执行内联。
  • @j_random_hacker 只是您通常将其用作指向基址的指针的情况。
  • @Ghita:即使那样,优化编译器也可能能够弄清楚对象的动态类型必须是什么。我的猜测是大多数编译器可以在Base* x = new Derived; x->foo(); 中内联对foo() 的调用。
  • @j_random_hacker 是的,它可以。但在正常用例中,您通常将指针传递给基类。有趣的是,最简单的情况由编译器处理。
  • 当完整的系统分析完成并且接口仅用于模块化和可测试性并且只有一个子类时,即使指向 base 的指针也可以通过 LTO 编译内联。不幸的是,目前我不确定这是否由任何编译器完成。
【解决方案3】:

与常规调用相比,每次调用虚函数都会有少量损失。除非您每秒执行数十万次调用,否则您不太可能观察到差异,而且无论如何,为增加代码清晰度付出代价通常是值得的。

【讨论】:

    【解决方案4】:

    当你调用一个虚函数(比如通过一个接口)时,程序必须在表中查找该函数,以查看为该对象调用哪个函数。与直接调用函数相比,这会带来很小的损失。

    此外,当您使用虚函数时,编译器无法内联函数调用。因此,对一些小函数使用虚函数可能会受到惩罚。这通常是您可能看到的最大性能“打击”。如果函数很小并且被多次调用,这真的只是一个问题,比如在一个循环中。

    【讨论】:

    • 确实不要让它搞砸你的设计,但只在你真正需要时才使用虚函数——当你考虑迭代(大量)元素时,内联会带来巨大的性能提升并在每一个上调用该方法。
    • 虚拟调用不能内联的说法是不正确的。只要编译器可以在编译时确定对象的最终类型,对该对象的方法调用就是内联的候选对象。只有在通过指向基址的指针调用时才能执行内联。
    【解决方案5】:

    在某些情况下适用的另一种选择是编译时 带模板的多态性。它很有用,例如,当你想要 在程序开始时做出实施选择,以及 然后在执行期间使用它。一个例子 运行时多态性

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

    【讨论】:

    • 不幸的是,这不适用于插件类和其他动态加载的类型(是的,这 在 C++ 中是可能的 :-)
    • 这增加了很多代码膨胀,我仍在等待任何检查模板代码膨胀对性能影响的研究(缓存问题、分支预测......)。
    【解决方案6】:

    我不认为虚拟函数调用和直接函数调用之间的成本比较。如果您正在考虑使用抽象基类(接口),那么您会遇到一种情况,即您希望根据对象的动态类型执行多个操作之一。你必须以某种方式做出选择。一种选择是使用虚函数。另一种是通过 RTTI(可能很昂贵)或向基类添加 type() 方法(可能会增加每个对象的内存使用)来切换对象的类型。因此,虚函数调用的成本应该与替代的成本进行比较,而不是与什么都不做的成本进行比较。

    【讨论】:

      【解决方案7】:

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

      但是,根据我从事大型项目的经验,清晰的接口和适当的封装所带来的好处很快就会抵消速度上的提升。模块化代码可以交换为改进的实现,因此最终结果是巨大的收益。

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

      【讨论】:

      • 我想提供一个反驳点:对于嵌入式系统(其他人提到了视频游戏机),性能影响可能太大,除非从可维护性/可读性/清晰性中获得很多收益等。应该避免它们。
      【解决方案8】:

      请注意,多重继承会使用多个 vtable 指针使对象实例膨胀。在 x86 上使用 G++,如果您的类有一个虚方法但没有基类,那么您有一个指向 vtable 的指针。如果你有一个带有虚方法的基类,你仍然有一个指向 vtable 的指针。如果你有两个带有虚方法的基类,你有 两个 vtable 指针在每个实例上

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

      【讨论】:

        【解决方案9】:

        需要注意的一点是,虚拟函数调用成本可能因平台而异。在控制台上它们可能会更明显,因为通常 vtable 调用意味着缓存未命中并且可能会破坏分支预测。

        【讨论】:

        • 我经常听到这种说法,但从未真正得到证实。全局性能应用程序性能的成本效益成本,无论是否在控制台中,实际上取决于您调用该方法的频率。我在游戏行业的大多数朋友都不允许完全代表你的论点使用虚函数。即使对于每个渲染周期调用一次的更高级别的函数,他们最终也会使用混乱的代码来规避这个限制。
        【解决方案10】:

        在 C++ 中使用抽象基类通常要求使用虚函数表,所有接口调用都将通过该表进行查找。与原始函数调用相比,成本很小,所以在担心它之前,请确保您需要比这更快。

        【讨论】:

          【解决方案11】:

          我知道的唯一主要区别是,由于您没有使用具体的类,因此内联(很多?)更难做到。

          【讨论】:

            【解决方案12】:

            我唯一能想到的是虚拟方法调用比非虚拟方法慢一点,因为调用必须通过virtual method table

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

            【讨论】:

              【解决方案13】:

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

              【讨论】:

                【解决方案14】:

                是的,但据我所知没有什么值得注意的。性能下降是因为您在每个方法调用中都有“间接”。

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

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

                【讨论】:

                  【解决方案15】:

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

                  【讨论】:

                    【解决方案16】:

                    我知道这是一个不常见的观点,但即使提到这个问题,我也会怀疑你对班级结构的考虑太多了。我见过许多具有太多“抽象级别”的系统,仅此一项就使它们容易出现严重的性能问题,这不是由于方法调用的成本,而是由于倾向于进行不必要的调用。如果这种情况发生在多个层次上,那就是杀手锏。 take a look

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2021-10-01
                      • 2016-12-20
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多