虚函数和性能C ++

时间:2011-02-02 07:52:18

标签: c++ performance inheritance virtual

在你对重复的标题感到畏缩之前,另一个问题不适合我在这里问的问题(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次迭代,结果令人担忧:非虚拟化为4ms,虚拟化为250ms!我几乎在里面“nooooooo”,但随后我将迭代次数增加到500,000左右;看到结果几乎完全相同(如果没有启用优化标志,可能会慢5%)。

我的问题是,与高额相比,为什么迭代次数较少会出现如此显着的变化?是纯粹因为虚拟函数在那么多次迭代中都在缓存中很热吗?

声明
我理解我的“分析”代码并不完美,但它实际上给出了对事物的估计,这在这个层面上是最重要的。此外,我要求学习这些问题,而不是仅仅优化我的应用程序。

8 个答案:

答案 0 :(得分:11)

我相信你的测试用例过于人为,不具备任何重要价值。

首先,在您的profiled函数中,您可以动态地分配和释放一个对象以及调用一个函数,如果您只想对函数调用进行分析,那么您应该这样做。

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

答案 1 :(得分:5)

扩展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

不太有说服力。

答案 2 :(得分:3)

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

答案 3 :(得分:2)

我认为这种测试实际上是无用的,实际上: 1)你正在浪费时间自我调用gettimeofday();
2)你没有真正测试虚函数,恕我直言这是最糟糕的事情。

为什么呢?因为您使用虚函数来避免编写如下内容:

<pseudocode>
switch typeof(object) {

case ClassA: functionA(object);

case ClassB: functionB(object);

case ClassC: functionC(object);
}
</pseudocode>

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

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

答案 4 :(得分:2)

时间差异可能有几个原因。

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

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

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

答案 5 :(得分:2)

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

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

答案 6 :(得分:1)

当使用太少的迭代时,测量中会产生很多噪音。 gettimeofday函数不够精确,只能为少量迭代提供良好的测量,更不用说它记录总的墙时间(包括被其他线程抢占所花费的时间)。

但是,最重要的是,你不应该想出一些可笑的复杂设计来避免虚函数。他们真的不会增加太多开销。如果您拥有令人难以置信的性能关键代码并且您知道虚拟功能在大多数时间构成,那么可能需要担心的事情。但是,在任何实际应用中,虚拟功能都不会使你的应用程序变慢。

答案 7 :(得分:0)

在我看来,当循环次数较少时,可能没有上下文切换,但是当你增加循环次数时,那么上下文切换发生的可能性非常大,并且主导了读取。例如,第一个程序需要1秒,第二个程序需要3秒,但如果上下文切换需要10秒,则差值为13/11而不是3/1。