虚函数性能:一个大类与许多较小的子类

时间:2014-04-09 23:41:39

标签: c++ performance

我正在重构我制作的c ++ OpenGL应用程序(技术上,这是一个在Qt的QQuickItem类中大量使用瘦OpenGL包装器的应用程序)。我的应用程序运行正常,但可能会更好。

我很好奇的一个问题涉及在非常时间敏感(帧速率)算法中使用virtual函数。我的OpenGL绘图代码在需要绘图的各种对象上调用许多virtual函数。由于这种情况每秒发生多次,我想知道virtual调度是否会降低帧速率。

我正在考虑改为使用这种结构,它通过将所有内容保存在一个基类中来避免继承,但以前的virtual函数现在只包含switch语句来调用基于class的“type”实际上只是typedef enum

此前:

struct Base{
  virtual void a()=0;
  virtual void b()=0;
}

struct One : public Base{
  void a(){...}
  void b(){...}
}

考虑到:

struct Combined{

  MyEnumTypeDef t; //essentially holds what "type" of object this is

  void a(){

    switch (t){

     case One:
       ....
       break;

      case Two:
       ....
       break;
     }
   }
  }

在OpenGL绘图例程中经常调用函数a()时,我很想认为Combined类效率会高得多,因为它不需要在虚拟表上进行动态调度。 / p>

如果这是明智的话,我会对这个问题提出一些建议。

3 个答案:

答案 0 :(得分:11)

在您的情况下,可能并不重要。我说可能是因为,我的意思是建设性的,你没有指定性能要求并且没有指定有问题的函数被调用的频率表明你可能没有足够的信息来做出判断 - 不要推测:个人资料"全局响应实际只是为了确保您拥有所需的所有必要信息,因为过早的微优化是非常常见的,我们的真正的目标是帮助您在大图片。

Jeremy Friesner用his comment on another answer here确实击中了头部:

  

如果你不明白为什么它很慢,你就无法加快速度。

所以,考虑到所有这些,假设A)您的性能要求已经满足(例如,您正在拉动4000 FPS - 远高于任何显示刷新率)或B)您正在努力满足性能要求和这个函数只被称为每帧几个(比如说< 1000-ish)次)或者C)你正在努力满足性能要求,这个函数经常被调用但是做了很多其他重要的工作(因此函数调用开销是可以忽略不计),然后:

使用virtual函数最多可能会在某个地方的某个表中进行一次额外的查找(可能还有一些缓存未命中 - 但如果在内部循环中重复访问则不会太多) ,这是几个CPU时钟周期最差的情况(并且很可能仍然小于switch,虽然这在这里真的没什么意义),并且与目标帧速率相比,完全无关紧要,需要的工作量渲染一个框架,以及您正在执行的任何其他算法和逻辑。如果您想要自己证明,请进行个人资料。

像这样的微优化不会产生影响,并且代码可维护性的成本即使很小,也不值得获益,基本上为零。

所做的是坐下来处理你的实际情况。您需要来提高性能吗?这个功能是否足以实际产生重大影响,或者您应该专注于其他技术(例如,更高级别的算法,其他设计策略,向GPU卸载计算或使用机器特定的优化,例如使用SSE的批量操作等。 )?

在没有具体信息的情况下,你可以做的一件事就是尝试这两种方法。虽然不同机器的性能会有所不同,但至少可以大致了解这一特定代码对整体性能的影响(例如,如果您为60 FPS拍摄,这两个选项会给你23.2 FPS与23.6 FPS相比,那么这并不是你想要关注的地方,而选择其中一种策略而不是另一种策略所做出的牺牲可能是不值得的。)

还考虑使用调用列表,顶点索引缓冲区等.OpenGL提供了许多用于优化对象绘制的工具,其中某些方面保持不变。例如,如果您有一个巨大的曲面模型,其中包含顶点坐标经常变化的小零件,请使用调用列表将模型划分为多个部分,并仅更新自上次重绘后更改的部分的调用列表。请假如果它们经常变化,则在调用列表中使用着色和纹理(或使用坐标数组)。这样你就可以避免完全调用你的函数。


如果您有点好奇,这是一个测试程序(可能并不代表您的实际使用情况,再次,这是不可能回答给出的信息 - 此测试是下面评论中要求的测试)。这意味着这些结果将反映在您的程序中,并且您需要获得有关实际要求的具体信息。但是,这只是为了咯咯笑:

此测试程序将基于开关的操作与基于虚函数的操作与指向成员的指针(其中成员从另一个类成员函数调用)与指向成员的指针(其中调用成员)进行比较直接来自测试循环)。它还执行三种类型的测试:在仅有一个运算符的数据集上运行,在两个运算符之间来回交替运行,以及使用两个运算符的随机混合的运行。

使用gcc -O0编译时的输出,持续1,000,000,000次迭代:

$ g++ -O0 tester.cpp
$ ./a.out 
--------------------
Test: time=6.34 sec (switch add) [-358977076]
Test: time=6.44 sec (switch subtract) [358977076]
Test: time=6.96 sec (switch alternating) [-281087476]
Test: time=18.98 sec (switch mixed) [-314721196]
Test: time=6.11 sec (virtual add) [-358977076]
Test: time=6.19 sec (virtual subtract) [358977076]
Test: time=7.88 sec (virtual alternating) [-281087476]
Test: time=19.80 sec (virtual mixed) [-314721196]
Test: time=10.96 sec (ptm add) [-358977076]
Test: time=10.83 sec (ptm subtract) [358977076]
Test: time=12.53 sec (ptm alternating) [-281087476]
Test: time=24.24 sec (ptm mixed) [-314721196]
Test: time=6.94 sec (ptm add (direct)) [-358977076]
Test: time=6.89 sec (ptm subtract (direct)) [358977076]
Test: time=9.12 sec (ptm alternating (direct)) [-281087476]
Test: time=21.19 sec (ptm mixed (direct)) [-314721196]

使用gcc -O3编译时的输出,持续1,000,000,000次迭代:

$ g++ -O3 tester.cpp ; ./a.out
--------------------
Test: time=0.87 sec (switch add) [372023620]
Test: time=1.28 sec (switch subtract) [-372023620]
Test: time=1.29 sec (switch alternating) [101645020]
Test: time=7.71 sec (switch mixed) [855607628]
Test: time=2.95 sec (virtual add) [372023620]
Test: time=2.95 sec (virtual subtract) [-372023620]
Test: time=14.74 sec (virtual alternating) [101645020]
Test: time=9.39 sec (virtual mixed) [855607628]
Test: time=4.20 sec (ptm add) [372023620]
Test: time=4.21 sec (ptm subtract) [-372023620]
Test: time=13.11 sec (ptm alternating) [101645020]
Test: time=9.32 sec (ptm mixed) [855607628]
Test: time=3.37 sec (ptm add (direct)) [372023620]
Test: time=3.37 sec (ptm subtract (direct)) [-372023620]
Test: time=13.08 sec (ptm alternating (direct)) [101645020]
Test: time=9.74 sec (ptm mixed (direct)) [855607628]

请注意-O3做了很多事情,如果不查看汇编程序,我们就无法将其用作手头问题的100%准确表示。

在未经优化的情况下,我们注意到:

  • 虚拟优于单个运营商的运行中的开关。
  • 在使用多个运算符的情况下,切换优于虚拟。
  • 直接调用成员时指向成员(object->*ptm_)与虚拟成员相似但速度慢。
  • 通过另一种方法(object->doit() doit()调用this->*ptm_)调用成员时,指向成员的指针占用的时间少了两倍。
  • 正如所料,"混合"由于分支预测失败,案例性能受到影响。

在优化案例中:

  • 在所有情况下,切换都优于虚拟。
  • 指向成员的类似特征为未经优化的案例。
  • 全部"交替"在某些时候涉及函数指针的情况比使用-O0慢并且慢于"混合"由于我不明白的原因。这不会发生在家里的电脑上。

这里特别重要的是例如多少的影响。分支预测胜过任何选择"虚拟"与"切换"。再次,确保您了解您的代码并优化正确的事情。

这里另一个重要的事情是这表示每次操作大约1-14纳秒的时间差。这种差异对于大量操作来说可能很重要,但与你正在做的其他事情相比可能是微不足道的(请注意,这些函数只执行一次算术运算,不过这将会使虚拟对开关的影响快速相形见绌)。

另请注意,在调用指向成员的指针时,直接显示了"改进"通过另一个类成员调用它,这对整体设计有潜在的巨大影响,因为这样的实现(至少在这种情况下,类外部的东西直接调用成员)不能作为另一个实现到期的直接替代调用指针到成员函数的不同语法(->->*)。例如,我必须创建一组完整的测试用例来处理它。

结论

即使是一些额外的算术运算,性能差异也很容易相形见绌。另请注意,除了虚拟交替"之外的所有分支预测都会产生更为显着的影响。案例与-O3。但是,测试也不太可能代表实际应用(OP保守秘密),并且-O3引入了更多变量,因此必须采取一定的结果并且不太可能适用对于其他场景(换句话说,测试可能很有趣,但不是特别有意义)。

<强>来源:

// === begin timing ===
#ifdef __linux__
#  include <sys/time.h>
typedef struct timeval Time;
static void tick (Time &t) {
  gettimeofday(&t, 0);
}
static double delta (const Time &a, const Time &b) {
  return
    (double)(b.tv_sec - a.tv_sec) +
    (double)(b.tv_usec - a.tv_usec) / 1000000.0;
}
#else // windows; untested, working from memory; sorry for compile errors
#  include <windows.h>
typedef LARGE_INTEGER Time;
static void tick (Time &t) {
  QueryPerformanceCounter(&t);
}
static double delta (const Time &a, const Time &b) {
  LARGE_INTEGER freq;
  QueryPerformanceFrequency(&freq);
  return (double)(b.QuadPart - a.QuadPart) / (double)freq.QuadPart;
}
#endif
// === end timing

#include <cstdio>
#include <cstdlib>
#include <ctime>

using namespace std;

// Size of dataset.
static const size_t DATASET_SIZE = 10000000;

// Repetitions per test.
static const unsigned REPETITIONS = 100;


// Class performs operations with a switch statement.
class OperatorSwitch {
public:
  enum Op { Add, Subtract };
  explicit OperatorSwitch (Op op) : op_(op) { }
  int perform (int a, int b) const {
    switch (op_) {
    case Add: return a + b;
    case Subtract: return a - b;
    }
  }
private:
  Op op_;
};


// Class performs operations with pointer-to-member.
class OperatorPTM {
public:
  enum Op { Add, Subtract };
  explicit OperatorPTM (Op op) {
    perform_ = (op == Add) ? 
      &OperatorPTM::performAdd :
      &OperatorPTM::performSubtract;
  }
  int perform (int a, int b) const { return (this->*perform_)(a, b); }
  int performAdd (int a, int b) const { return a + b; }
  int performSubtract (int a, int b) const { return a - b; }
  //private:
  int (OperatorPTM::*perform_) (int, int) const;
};


// Base class for virtual-function test operator.
class OperatorBase {
public:
  virtual ~OperatorBase () { }
  virtual int perform (int a, int b) const = 0;
};

// Addition
class OperatorAdd : public OperatorBase {
public:
  int perform (int a, int b) const { return a + b; }
};

// Subtraction
class OperatorSubtract : public OperatorBase {
public:
  int perform (int a, int b) const { return a - b; }
};


// No base

// Addition
class OperatorAddNoBase {
public:
  int perform (int a, int b) const { return a + b; }
};

// Subtraction
class OperatorSubtractNoBase {
public:
  int perform (int a, int b) const { return a - b; }
};



// Processes the dataset a number of times, using 'oper'.
template <typename T>
static void test (const int *dataset, const T *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
      result = oper->perform(result, dataset[i]);

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


// Processes the dataset a number of times, alternating between 'oper[0]'
// and 'oper[1]' per element.
template <typename T>
static void testalt (const int *dataset, const T * const *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
      result = oper[i&1]->perform(result, dataset[i]);

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


// Processes the dataset a number of times, choosing between 'oper[0]'
// and 'oper[1]' randomly (based on value in dataset).
template <typename T>
static void testmix (const int *dataset, const T * const *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
      int d = dataset[i];
      result = oper[d&1]->perform(result, d);
    }

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


// Same as test() but calls perform_() pointer directly.
static void test_ptm (const int *dataset, const OperatorPTM *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i)
      result = (oper->*(oper->perform_))(result, dataset[i]);

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


// Same as testalt() but calls perform_() pointer directly.
static void testalt_ptm (const int *dataset, const OperatorPTM * const *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
      const OperatorPTM *op = oper[i&1];
      result = (op->*(op->perform_))(result, dataset[i]);
    }

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


// Same as testmix() but calls perform_() pointer directly.
static void testmix_ptm (const int *dataset, const OperatorPTM * const *oper, const char *name) {

  int result = 0;
  Time start, stop;

  tick(start);

  for (unsigned n = 0; n < REPETITIONS; ++ n)
    for (size_t i = 0; i < DATASET_SIZE; ++ i) {
      int d = dataset[i];
      const OperatorPTM *op = oper[d&1];
      result = (op->*(op->perform_))(result, d);
    }

  tick(stop);

  // result is computed and printed so optimizations do not discard it.
  printf("Test: time=%.2f sec (%s) [%i]\n", delta(start, stop), name, result);
  fflush(stdout);

}


int main () {

  int *dataset = new int[DATASET_SIZE];
  srand(time(NULL));
  for (int n = 0; n < DATASET_SIZE; ++ n)
    dataset[n] = rand();

  OperatorSwitch *switchAdd = new OperatorSwitch(OperatorSwitch::Add);
  OperatorSwitch *switchSub = new OperatorSwitch(OperatorSwitch::Subtract);
  OperatorSwitch *switchAlt[2] = { switchAdd, switchSub };
  OperatorBase *virtAdd = new OperatorAdd();
  OperatorBase *virtSub = new OperatorSubtract();
  OperatorBase *virtAlt[2] = { virtAdd, virtSub };
  OperatorPTM *ptmAdd = new OperatorPTM(OperatorPTM::Add);
  OperatorPTM *ptmSub = new OperatorPTM(OperatorPTM::Subtract);
  OperatorPTM *ptmAlt[2] = { ptmAdd, ptmSub };

  while (true) {
    printf("--------------------\n");
    test(dataset, switchAdd, "switch add");
    test(dataset, switchSub, "switch subtract");
    testalt(dataset, switchAlt, "switch alternating");
    testmix(dataset, switchAlt, "switch mixed");
    test(dataset, virtAdd, "virtual add");
    test(dataset, virtSub, "virtual subtract");
    testalt(dataset, virtAlt, "virtual alternating");
    testmix(dataset, virtAlt, "virtual mixed");
    test(dataset, ptmAdd, "ptm add");
    test(dataset, ptmSub, "ptm subtract");
    testalt(dataset, ptmAlt, "ptm alternating");
    testmix(dataset, ptmAlt, "ptm mixed");
    test_ptm(dataset, ptmAdd, "ptm add (direct)");
    test_ptm(dataset, ptmSub, "ptm subtract (direct)");
    testalt_ptm(dataset, ptmAlt, "ptm alternating (direct)");
    testmix_ptm(dataset, ptmAlt, "ptm mixed (direct)");
  }

}

答案 1 :(得分:1)

不使用虚拟功能会更快,但差异是否显着是很难说的。您应该通过分析器运行程序,以查看它花费时间的位置。您可能会发现cpu循环花费在完全不同的东西上,并且你会通过弄乱虚拟来浪费你的时间(并降低你的设计)。

Unix:How can I profile C++ code running in Linux? Windows:What's the best free C++ profiler for Windows?

另一个需要考虑的选择可能是使用奇怪的重复模板模式:http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern来获得类似的多态而不使用虚拟。

答案 2 :(得分:1)

“吸引自己的许多物品”的模型很有吸引力,但却以偷偷摸摸的方式变坏。它不是虚函数调用开销(存在,但很小),它鼓励渲染反模式:让每个对象孤立地绘制自己。这听起来像是“软件工程最佳实践”中吹捧的那些东西之一,但它不是,它非常糟糕。每个对象都会进行大量昂贵的API调用(例如绑定着色器和纹理)。现在,我真的不知道你的代码是什么样的,也许它不会像这样工作,对象不一定是坏的,这就是它们的使用方式。

无论如何,这里有一些建议。

按照他们想要的状态(着色器,纹理,顶点缓冲区,按顺序)对对象进行排序(实际上,不进行排序 - 将它们放入存储桶并迭代它们)。这很容易,每个人都这样做,而且可能就足够了。

合并状态,因此无需切换。使用übershaders。使用纹理数组,或者更好的无绑定纹理(没有所有切片必须具有相同格式/大小/等的问题)。使用一个巨大的顶点缓冲区,将所有内容放入其中。使用统一缓冲区。对动态缓冲区使用持久映射。

最后,glMultiDrawElementsIndirect。如果按照之前的建议将所有内容放入缓冲区,那么您只需要很少调用glMultiDrawElementsIndirect非常很少,你可以通过一次通话完成很多工作。你可能会使用的是一堆glDrawArrays,它们之间没有任何约束力,这也不错,但要让它变得更好,并没有太大的努力。

最终结果是实际绘图代码几乎消失了。几乎所有API调用都消失了,取而代之的是对缓冲区的写入。