加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-16 10:34:31 所属栏目:百科 来源:网络整理
导读:我正在重构我制作的c OpenGL应用程序(技术上,这是一个在Qt的QQuickItem类中大量使用瘦OpenGL包装器的应用程序).我的应用程序运行正常,但可能会更好. 我很好奇的一个问题涉及在非常时间敏感(帧速率)算法中使用虚函数.我的OpenGL绘图代码在需要绘图的各种对象
我正在重构我制作的c OpenGL应用程序(技术上,这是一个在Qt的QQuickItem类中大量使用瘦OpenGL包装器的应用程序).我的应用程序运行正常,但可能会更好.

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

我正在考虑更改为这个结构而不是通过将所有内容保存在一个基类中来避免继承,但是先前的虚函数现在只包含switch语句以根据类的“类型”调用适当的例程,这实际上只是一个typedef枚举:

先前:

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类将更加高效,因为它不需要在虚拟表上进行动态调度.

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

解决方法

在你的情况下,它可能没关系.我说可能是因为,我的意思是建设性的,你没有指定性能要求,也没有说明调用相关函数的频率,这表明你现在可能没有足够的信息来做出判断 – “不要” t推测:配置文件“毯子响应实际上只是为了确保您拥有所需的所有必要信息,因为过早的微优化是非常常见的,我们的真正目标是帮助您全面了解.

Jeremy Friesner用his comment on another answer here击中头部钉子:

If you don’t understand why it is slow you won’t be able to speed it up.

因此,考虑到所有这些,假设A)您的性能要求已经满足(例如,您正在拉动4000 FPS – 远高于任何显示器刷新率)或B)您正在努力满足性能要求并且此功能是每帧只调用一些(比如说< 1000-ish)次数)或者C)你正在努力满足性能要求并经常调用这个函数,但是做了很多其他重要的工作(因此函数调用开销可以忽略不计),然后: 使用虚函数最多可能最终会在某个表中的一个额外查找中结束(可能还有一些缓存未命中 – 但如果在内部循环中重复访问则不是很多),这是几个CPU时钟循环最坏情况(并且很可能仍然少于一个开关,虽然这在这里真的没什么意义),与你的目标帧速率,渲染帧所需的工作量以及你正在执行的任何其他算法和逻辑相比,这是完全无关紧要的.如果你想向自己证明,请介绍一下. 你应该做的是使用任何技术导致最清晰,最干净,最易维护和可读的代码.诸如此类的微优化不会产生影响,并且代码可维护性的成本即使很小,也不值得获益,基本上为零. 你还应该做的就是坐下来处理你的实际情况.你需要提高性能吗?这个功能是否足以实际产生重大影响,或者您应该专注于其他技术(例如,更高级别的算法,其他设计策略,向GPU卸载计算或使用机器特定的优化,例如使用SSE的批量操作等. )? 在没有具体信息的情况下,你可以做的一件事就是尝试这两种方法.虽然不同机器的性能会有所不同,但至少可以大致了解这一特定代码对整体性能的影响(例如,如果你的射击速度为60 FPS,这两个选项可以给你23.2 FPS vs 23.6 FPS,那么这不是你想要关注的地方,通过选择其中一种策略而不是另一种策略可能做出的牺牲可能不值得). 还要考虑使用调用列表,顶点索引缓冲区等.OpenGL提供了许多用于优化对象绘制的工具,其中某些方面保持不变.例如,如果您有一个巨大的曲面模型,其中包含顶点坐标经常变化的小零件,请使用调用列表将模型划分为多个部分,并仅更新自上次重绘后更改的部分的调用列表.请假如果它们经常变化,则在调用列表中使用着色和纹理(或使用坐标数组).这样你就可以避免完全调用你的函数. 如果你很好奇,这里有一个测试程序(可能并不代表你的实际使用情况,再次,这是不可能回答给出的信息 – 这个测试是下面评论中要求的那个).这并不意味着这些结果会反映在您的程序中,您需要获得有关实际需求的具体信息.但是,这只是为了咯咯笑: 此测试程序将基于开关的操作与基于虚函数的操作与指向成员的指针(其中成员从另一个类成员函数调用)与指向成员的指针(其中成员直接从test调用)进行比较环).它还执行三种类型的测试:在仅有一个运算符的数据集上运行,在两个运算符之间来回交替运行,以及使用两个运算符的随机混合的运行. 使用gcc -O0编译时的输出,进行1,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编译时的输出,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_)与virtual相似但速度慢.
>通过另一种方法调用成员时指针成员(object-> doit(),其中doit()调用this-> * ptm_)需要两倍的时间.
>正如预期的那样,由于分支预测失败,“混合”案例性能受到影响.

在优化的情况下:

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

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

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

还要注意,虽然调用指向成员的指针直接显示通过另一个类成员调用它的“改进”,但这对整体设计有潜在的巨大影响,因为这样的实现(至少在这种情况下,类外的东西调用由于调用指针到成员函数的语法不同( – > vs. – > *),因此无法直接替换成员作为另一个实现的直接替换.例如,我必须创建一组完整的测试用例来处理它.

结论

即使是一些额外的算术运算,性能差异也很容易相形见绌.另请注意,除了-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,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,choosing between 'oper[0]'
// and 'oper[1]' randomly (based on value in dataset).
template <typename T>
static void testmix (const int *dataset,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",result);
  fflush(stdout);

}


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

  tick(start);

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

}


// Same as testalt() but calls perform_() pointer directly.
static void testalt_ptm (const int *dataset,const OperatorPTM * const *oper,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",result);
  fflush(stdout);

}


// Same as testmix() but calls perform_() pointer directly.
static void testmix_ptm (const int *dataset,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,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,"switch mixed");
    test(dataset,virtAdd,"virtual add");
    test(dataset,virtSub,"virtual subtract");
    testalt(dataset,virtAlt,"virtual alternating");
    testmix(dataset,"virtual mixed");
    test(dataset,ptmAdd,"ptm add");
    test(dataset,ptmSub,"ptm subtract");
    testalt(dataset,ptmAlt,"ptm alternating");
    testmix(dataset,"ptm mixed");
    test_ptm(dataset,"ptm add (direct)");
    test_ptm(dataset,"ptm subtract (direct)");
    testalt_ptm(dataset,"ptm alternating (direct)");
    testmix_ptm(dataset,"ptm mixed (direct)");
  }

}

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读