【问题标题】:Virtual Functions and Performance C++虚函数和性能 C++
【发布时间】:2011-06-19 19:22:53
【问题描述】:

在您对重复的标题感到畏缩之前,另一个问题不适合我在这里提出的问题 (IMO)。所以。

我真的很想在我的应用程序中使用虚函数来让事情变得简单一百倍(这不就是 OOP 的全部内容吗;))。但是我在某处读到它们是以性能为代价的,只看到过早优化的旧人为炒作,我决定在一个小型基准测试中快速旋转它,使用:

CProfiler.cpp

#include "CProfiler.h"

CProfiler::CProfiler(void (*func)(void), unsigned int iterations) {
    gettimeofday(&a, 0);
    for (;iterations > 0; iterations --) {
        func();
    }
    gettimeofday(&b, 0);
    result = (b.tv_sec * (unsigned int)1e6 + b.tv_usec) - (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
};

main.cpp

#include "CProfiler.h"

#include <iostream>

class CC {
  protected:
    int width, height, area;
  };

class VCC {
  protected:
    int width, height, area;
  public:
    virtual void set_area () {}
  };

class CS: public CC {
  public:
    void set_area () { area = width * height; }
  };

class VCS: public VCC {
  public:
    void set_area () {  area = width * height; }
  };

void profileNonVirtual() {
    CS *abc = new CS;
    abc->set_area();
    delete abc;
}

void profileVirtual() {
    VCS *abc = new VCS;
    abc->set_area();
    delete abc;
}

int main() {
    int iterations = 5000;
    CProfiler prf2(&profileNonVirtual, iterations);
    CProfiler prf(&profileVirtual, iterations);

    std::cout << prf.result;
    std::cout << "\n";
    std::cout << prf2.result;

    return 0;
}

一开始我只进行了 100 和 10000 次迭代,结果令人担忧:非虚拟化 4 毫秒,虚拟化 250 毫秒!我几乎进入了“nooooooo”,但后来我将迭代次数提高到了大约 500,000 次;看到结果变得几乎完全相同(在没有启用优化标志的情况下可能会慢 5%)。

我的问题是,为什么与大量迭代相比,少量迭代会有如此显着的变化?纯粹是因为虚拟函数在这么多次迭代时在缓存中很热吗?

免责声明
我知道我的“分析”代码并不完美,但它提供了对事物的估计,这在这个级别上是最重要的。此外,我问这些问题是为了学习,而不是仅仅为了优化我的应用程序。

【问题讨论】:

  • "(在不启用优化标志的情况下可能会慢 5%)"——这意味着您正在分析调试/非优化构建。这样做会产生一个通常远没有缺陷的基准。是这样吗?
  • 它在 Ubuntu 10.10 上出现故障,使用 g++, 没有优化标志。
  • 您肯定在编译时启用了优化以进行基准测试,对吧?
  • @Daniel 你应该删除新/删除;在分析循环之外预分配对象,进行真正的虚拟调用(即 ptr2base->set_area()),然后运行足够的测试迭代(至少几秒钟),然后使用适当的 cmets 将新结果添加到您的问题中:)跨度>

标签: c++ performance inheritance virtual


【解决方案1】:

在我看来,当循环次数较少时,可能没有上下文切换,但是当你增加循环次数时,上下文切换发生的可能性很大,这在阅读中占主导地位。例如第一个程序需要 1 秒,第二个程序需要 3 秒,但如果上下文切换需要 10 秒,那么差异是 13/11 而不是 3/1。

【讨论】:

    【解决方案2】:

    扩展Charles' answer

    这里的问题是,您的循环所做的不仅仅是测试虚拟调用本身(内存分配可能会使虚拟调用开销相形见绌),所以他的建议是更改代码以便只测试虚拟调用。

    这里的基准函数是模板,因为模板可能是内联的,而通过函数指针调用不太可能。

    template <typename Type>
    double benchmark(Type const& t, size_t iterations)
    {
      timeval a, b;
      gettimeofday(&a, 0);
      for (;iterations > 0; --iterations) {
        t.getArea();
      }
      gettimeofday(&b, 0);
      return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) -
             (a.tv_sec * (unsigned int)1e6 + a.tv_usec);
    }
    

    类:

    struct Regular
    {
      Regular(size_t w, size_t h): _width(w), _height(h) {}
    
      size_t getArea() const;
    
      size_t _width;
      size_t _height;
    };
    
    // The following line in another translation unit
    // to avoid inlining
    size_t Regular::getArea() const { return _width * _height; }
    
    struct Base
    {
      Base(size_t w, size_t h): _width(w), _height(h) {}
    
      virtual size_t getArea() const = 0;
    
      size_t _width;
      size_t _height;
    };
    
    struct Derived: Base
    {
      Derived(size_t w, size_t h): Base(w, h) {}
    
      virtual size_t getArea() const;
    };
    
    // The following two functions in another translation unit
    // to avoid inlining
    size_t Derived::getArea() const  { return _width * _height; }
    
    std::auto_ptr<Base> generateDerived()
    {
      return std::auto_ptr<Base>(new Derived(3,7));
    }
    

    以及测量:

    int main(int argc, char* argv[])
    {
      if (argc != 2) {
        std::cerr << "Usage: %prog iterations\n";
        return 1;
      }
    
      Regular regular(3, 7);
      std::auto_ptr<Base> derived = generateDerived();
    
      double regTime = benchmark<Regular>(regular, atoi(argv[1]));
      double derTime = benchmark<Base>(*derived, atoi(argv[1]));
    
      std::cout << "Regular: " << regTime << "\nDerived: " << derTime << "\n";
    
      return 0;
    }
    

    注意:与常规函数相比,这测试了虚拟调用的开销。功能不同(因为在第二种情况下您没有运行时分派),但这是最坏情况下的开销。

    编辑

    运行结果(gcc.3.4.2,-O2,SLES10 四核服务器)注意:在另一个翻译单元中定义函数,以防止内联

    > ./test 5000000
    Regular: 17041
    Derived: 17194
    

    没有说服力。

    【讨论】:

    • 感谢您的回答;但结果毫无意义,仅仅消除语法错误就花了 5 分钟。您的代码(已修复)在这里pastie.org/1520895,让我知道所需的更改是否中断。结果是常规:1 派生:5,000,000 次迭代的 12548。
    • @Daniel:我已经反向移植了 3 个修复程序,谢谢。你的结果看起来很奇怪(至少可以这么说),我看看能不能找到一些时间来运行它。
    • @Daniel:我花时间实际编译和运行代码。小心,正如我在 cmets 中所指出的,将函数定义放在另一个 .cpp 文件中。我已经发布了结果,正如你所看到的......根本没有太大区别。解释很可能是缓存在我的机器上起作用。
    • 我假设“另一个翻译单元”是指另一个文件?抱歉 :) 此外,所有的事情都说了和做了,结果看起来与我从 OP 得到的结果相同。 :P
    • @Daniel:是的,另一个“.cpp”。否则编译器可能会简单地放弃对函数的调用并用它的主体“就地”替换它(这称为内联,非常类似于宏,但具有类型安全性)。编译器还可能检测到使用了Derived,即使调用源自对Base 的引用,并且再次使用直接调用(而不是虚拟调用)甚至内联调用。通过使用另一个源文件,我们隐藏了这些信息,除非它使用链接时间优化,否则它实际上无法优化这些调用。
    【解决方案3】:

    时间差异可能有多种原因。

    • 您的计时功能不够精确
    • 堆管理器可能会影响结果,因为sizeof(VCS) &gt; sizeof(VS)。如果将new / delete 移出循环会发生什么?

    • 同样,由于大小差异,内存缓存可能确实是时间差异的一部分。

    但是:你真的应该比较类似的功能。使用虚函数时,您这样做是有原因的,即根据对象的身份调用不同的成员函数。如果您需要此功能,并且不想使用虚拟功能,则必须手动实现它,无论是使用函数表还是使用 switch 语句。这也是有代价的,这就是您应该与虚函数进行比较的原因。

    【讨论】:

    • 我使用了新的在那里测试了这两个功能,当只比较功能或只比较分配时,结果是相似的。即超过 5,000,000 次迭代,虚拟化测试平均慢 9%(测试 10 次并取平均值,因此实际上是 50,000,000)。
    • 是的,但您分配的内存量不同。因此,由于堆管理器,任何事情都可能发生。
    • 我如何分配内存?声明在堆栈上,纯粹是函数调用......(注意我的措辞:“仅比较函数或仅比较分配时结果相似”)。
    • CS *abc = new CS; 中,您在堆上分配CS
    • 请注意我的评论(再次):“仅比较函数时”。我做了单独的测试。检查我的 cmets 到 Charles Bailey 的回复。
    【解决方案4】:

    调用虚函数会对性能产生影响,因为它比调用常规函数稍微多一点。然而,在现实世界的应用程序中,这种影响可能完全可以忽略不计——甚至比在最精心设计的基准测试中出现的影响还要小。

    在现实世界的应用程序中,虚拟函数的替代方案通常会涉及到你手写一些类似的系统,因为调用虚拟函数和调用非虚拟函数的行为不同 - 前者改变了基于调用对象的运行时类型。您的基准测试,即使忽略它的任何缺陷,也不会测量等效行为,仅测量等效语法。如果您要制定禁止虚函数的编码策略,您要么必须编写一些可能非常迂回或令人困惑的代码(可能会更慢),要么重新实现编译器用来实现虚函数的类似类型的运行时调度系统函数行为(在大多数情况下,这肯定不会比编译器快)。

    【讨论】:

    • 感谢您的回答,但问题不在于是否有成本,而是为什么低迭代与高迭代的差异。 一致的差异。
    【解决方案5】:

    当迭代次数太少时,测量中会出现很多噪音。 gettimeofday 函数不够准确,无法仅在少数迭代中为您提供良好的测量结果,更不用说它记录了总的挂墙时间(包括被其他线程抢占时所花费的时间)。

    不过,最重要的是,您不应该想出一些可笑的复杂设计来避免使用虚函数。他们真的不会增加太多的开销。如果您有非常关键的性能代码,并且您知道虚拟函数占大多数时间,那么也许需要担心。不过,在任何实际应用中,虚函数都不会导致您的应用变慢。

    【讨论】:

    • 感谢您的谦虚回答,记住我说过,这不是为了优化我的应用程序,纯粹是出于兴趣,我只担心使用 Valgrind 时会出现应用程序级别的优化问题,Gprof 指出了这些问题.
    【解决方案6】:

    我认为您的测试用例过于人为,没有任何价值。

    首先,在您的分析函数中,您动态分配和解除分配一个对象以及调用一个函数,如果您只想分析函数调用,那么您应该这样做。

    其次,您没有分析虚拟函数调用代表给定问题的可行替代方案的情况。虚函数调用提供动态调度。您应该尝试分析一个案例,例如使用虚拟函数调用作为使用开关类型反模式的替代方案。

    【讨论】:

    • 所以你是说我应该在更复杂(虚拟化)的继承结构上尝试相同的测试?还必须注意这不是我的实际应用程序,它纯粹是功能测试。我不反对尝试不同的例子。
    • +1:注意。只是想把额外的压力放在只是做调用位。甚至不要将对象放在堆栈上,因为这样您每次都要计算对象的构造成本(创建一次)。
    • +1:(如果可以的话)。比较苹果和橙子是没有用的。将虚拟调度与替代方案进行比较(根据运行时确定的对象类型进行调用)。
    • 我相信 Charles 是在告诉你在解决动态调度问题的情况下编写带有和不带有虚拟方法的代码。
    • 不,不是,您没有使用此代码完成动态调度。动态调度允许为不同的类调用不同的函数。您需要双方都继承,并在非虚拟端进行类型检查和操作。
    【解决方案7】:

    我认为这种测试其实没什么用:
    1)您正在浪费时间来分析自己调用gettimeofday();
    2)您并没有真正测试虚拟功能,恕我直言,这是最糟糕的事情。

    为什么?因为您使用虚函数来避免编写诸如:

    <pseudocode>
    switch typeof(object) {
    
    case ClassA: functionA(object);
    
    case ClassB: functionB(object);
    
    case ClassC: functionC(object);
    }
    </pseudocode>
    

    在这段代码中,您错过了“if...else”块,因此您并没有真正获得虚函数的优势。在这种情况下,他们总是对非虚拟“失败者”。

    要进行正确的分析,我认为您应该添加类似于我发布的代码的内容。

    【讨论】:

      【解决方案8】:

      通过少量迭代,您的代码有可能被其他并行运行的程序抢占,或者发生交换或任何其他操作系统将您的程序与发生隔离的情况,您将有时间被操作暂停系统包含在您的基准测试结果中。这就是为什么你应该运行你的代码大约一千万次以或多或少可靠地测量任何东西的第一个原因。

      【讨论】:

      • 然而这些结果是一致的,我并没有仅仅尝试“一次”。
      • @Daniel:它可以是你无法控制的任何事情。试图有意义地分析它是浪费时间。如果你把它当作噪音,你会过得更好。
      猜你喜欢
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-17
      • 2013-06-25
      • 2012-12-20
      • 2015-08-23
      • 1970-01-01
      相关资源
      最近更新 更多