【问题标题】:Avoiding virtual functions避免虚函数
【发布时间】:2011-06-15 16:02:36
【问题描述】:

所以假设我想创建一系列类,每个类都有一个具有相同内容的成员函数。让我们调用函数

void doYourJob();

我想最终将所有这些类放入同一个容器中,这样我就可以遍历它们并让每个类都执行 'doYourJob()'

显而易见的解决方案是用函数制作一个抽象类

 virtual void doYourJob();

但我犹豫不决。这是一个耗时的程序,而虚函数会大大减少它。此外,这个函数是类之间唯一的共同点,并且 doYourJob 对每个类的实现完全不同。

有没有办法避免使用带有虚函数的抽象类,还是我不得不接受它?

【问题讨论】:

  • 使用多态类实现它。调度对虚拟函数的调用的开销很可能是微不足道的。在最坏的情况下,它很重要,而且您至少会有一个干净的设计,可以相对轻松地进行优化。

标签: c++ code-organization


【解决方案1】:

我担心循环中的一系列dynamic_cast 检查会使性能比虚函数更差。如果您要将它们全部放入一个容器中,它们需要有某种共同的类型,因此您不妨将其设置为包含该方法的纯虚拟基类。

在这种情况下,虚函数调度并没有那么多:vtable 查找、对提供的 this 指针的调整以及间接调用。

如果性能如此关键,您也许可以为每个子类型使用单独的容器并独立处理每个容器。如果顺序很重要,你会做很多后空翻,以至于虚拟调度可能更快。

【讨论】:

  • dynamic_cast 仅适用于具有至少一个虚函数的类型,如果是这种情况,您最好只为 doYourJob() 使用该虚函数。
  • 会像使用 [code]class base_class{virtual void doYourJob(){}}[code] 一样简单,还是有什么方法可以提高效率
  • 真的就是这么简单。您可能想改用virtual void doYourJob() = 0;,以便子类必须提供实现而不是获取默认的无操作实现。在任何情况下,代价是您为每个类添加一个指向sizeof 的指针,并且必须在调用时进行一两次间接寻址。几纳秒。
【解决方案2】:

如果您要将所有这些对象存储在同一个容器中,那么您将不得不编写一个异构容器类型(缓慢且昂贵),您将不得不存储一个容器void *s(糟糕!),否则这些类将不得不通过继承相互关联。如果您选择使用前两个选项中的任何一个,则必须有一些逻辑来查看容器中的每个元素,找出它是什么类型,然后调用适当的 doYourJob() 实现,它本质上归结为继承。

我强烈建议首先尝试使用继承的简单、直接的方法。如果这足够快,那就太好了!你完成了。如果不是,请尝试使用其他方案。永远不要因为成本而回避有用的语言功能,除非你有一些好的证据表明成本太高。

【讨论】:

    【解决方案3】:

    虚拟功能的成本并不高。它们是间接调用,基本上就像函数指针。 What is the performance cost of having a virtual method in a C++ class?

    如果您处于每次调用的每个周期都很重要的情况,也就是说您在函数调用中做的工作很少,并且您在性能关键应用程序中从内部循环调用它,您可能需要一个不同的完全接近。

    【讨论】:

      【解决方案4】:

      如果您需要速度,请考虑在对象中嵌入“类型(识别)编号”,并使用 switch 语句选择特定类型的代码。这可以完全避免函数调用开销——只需进行本地跳转。你不会比这更快。成本(在可维护性、重新编译依赖性等方面)是强制本地化(在开关中)特定类型的功能。


      实施

      #include <iostream>
      #include <vector>
      
      // virtual dispatch model...
      
      struct Base
      {
          virtual int f() const { return 1; }
      };
      
      struct Derived : Base
      {
          virtual int f() const { return 2; }
      };
      
      // alternative: member variable encodes runtime type...
      
      struct Type
      {
          Type(int type) : type_(type) { }
          int type_;
      };
      
      struct A : Type
      {
          A() : Type(1) { }
          int f() const { return 1; }
      };
      
      struct B : Type
      {
          B() : Type(2) { }
          int f() const { return 2; }
      };
      
      struct Timer
      {
          Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
          struct timespec from;
          double elapsed() const
          {
              struct timespec to;
              clock_gettime(CLOCK_MONOTONIC, &to);
              return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
          }
      };
      
      int main(int argc)
      {
        for (int j = 0; j < 3; ++j)
        {
          typedef std::vector<Base*> V;
          V v;
      
          for (int i = 0; i < 1000; ++i)
              v.push_back(i % 2 ? new Base : (Base*)new Derived);
      
          int total = 0;
      
          Timer tv;
      
          for (int i = 0; i < 100000; ++i)
              for (V::const_iterator i = v.begin(); i != v.end(); ++i)
                  total += (*i)->f();
      
          double tve = tv.elapsed();
      
          std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';
      
          // ----------------------------
      
          typedef std::vector<Type*> W;
          W w;
      
          for (int i = 0; i < 1000; ++i)
              w.push_back(i % 2 ? (Type*)new A : (Type*)new B);
      
          total = 0;
      
          Timer tw;
      
          for (int i = 0; i < 100000; ++i)
              for (W::const_iterator i = w.begin(); i != w.end(); ++i)
              {
                  if ((*i)->type_ == 1)
                      total += ((A*)(*i))->f();
                  else
                      total += ((B*)(*i))->f();
              }
      
          double twe = tw.elapsed();
      
          std::cout << "switched: " << total << ' ' << twe << '\n';
      
          // ----------------------------
      
          total = 0;
      
          Timer tw2;
      
          for (int i = 0; i < 100000; ++i)
              for (W::const_iterator i = w.begin(); i != w.end(); ++i)
                  total += (*i)->type_;
      
          double tw2e = tw2.elapsed();
      
          std::cout << "overheads: " << total << ' ' << tw2e << '\n';
        }
      }
      

      性能结果

      在我的 Linux 系统上:

      ~/dev  g++ -O2 -o vdt vdt.cc -lrt
      ~/dev  ./vdt                     
      virtual dispatch: 150000000 1.28025
      switched: 150000000 0.344314
      overhead: 150000000 0.229018
      virtual dispatch: 150000000 1.285
      switched: 150000000 0.345367
      overhead: 150000000 0.231051
      virtual dispatch: 150000000 1.28969
      switched: 150000000 0.345876
      overhead: 150000000 0.230726
      

      这表明内联类型数字切换方法的速度大约是 (1.28 - 0.23) / (0.344 - 0.23) = 9.2 倍。当然,这特定于测试的确切系统/编译器标志和版本等,但通常是指示性的。


      评论重新虚拟发送

      必须说,尽管虚函数调用开销很少是重要的,而且仅适用于经常调用的琐碎函数(如 getter 和 setter)。即使这样,您也可以提供一个函数来一次获取和设置很多东西,从而最大限度地降低成本。人们太担心虚拟调度——所以在找到尴尬的替代方案之前做分析。它们的主要问题是它们执行了离线函数调用,尽管它们也会使执行的代码离域,这会改变缓存使用模式(更好或更经常)。

      【讨论】:

      • 为什么使用 -lrt 和没有 clang++ 的代码会快得多? coliru.stacked-crooked.com/a/83ac0dc7d15b4747
      • 标志-lrt是什么,与库“rt”链接??
      • @Gabriel:是的——它会链接 librt.so——“rt”代表实时,包含clock_gettime 函数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-10-09
      • 2014-04-03
      • 1970-01-01
      • 2013-04-09
      • 1970-01-01
      • 2014-06-10
      • 1970-01-01
      相关资源
      最近更新 更多