接口范式性能(动态绑定与泛型编程)

bit*_*ask 7 c++ metaprogramming dynamic-binding

虽然它们的核心动态绑定和模板是根本不同的东西,但它们可用于实现相同的功能.

代码示例(仅供参考)

A)动态绑定

namespace DB {
  // interface
  class CustomCode {
    public:
      virtual void operator()(char) const = 0;
  };
  class Lib {
    public:
      void feature(CustomCode const& c) {
        c('d');
      }
  };

  // user code
  class MyCode1 : public CustomCode {
    public:
      void operator()(char i) const {
        std::cout << "1: " << i << std::endl;
      }
  };
  class MyCode2 : public CustomCode {
    public:
      void operator()(char i) const {
        std::cout << "2: " << i << std::endl;
      }
  };

  void use() {
    Lib lib;
    lib.feature(MyCode1());
    lib.feature(MyCode2());
  }
}
Run Code Online (Sandbox Code Playgroud)

B)通用编程

namespace GP {
  //interface
  template <typename CustomCode> class Lib {
    public:
      void feature(CustomCode const& c) {
        c('g');
      }
  };

  // user code
  class MyCode1 {
    public:
      void operator()(char i) const {
        std::cout << "1: " << i << std::endl;
      }
  };
  class MyCode2 {
    public:
      void operator()(char i) const {
        std::cout << "2: " << i << std::endl;
      }
  };

  void use() {
    Lib<MyCode1> lib;
    lib.feature(MyCode1());
    //lib.feature(MyCode2());  <-- illegal
  }
}
Run Code Online (Sandbox Code Playgroud)

一些想法

虽然这些范例并不相同并且各有优缺点(A有点强大(参见参考资料MyCode2)并且B对用户来说更灵活),但它们都允许实现相同的功能(虽然上面提到的限制适用).

无论如何,理论上(TM)A在运行时由于虚函数的间接性而稍慢,同时B提供了一些很好的优化机会,因为方法可以内联(当然你没有间接).
但是,我经常觉得这A有点自我记录,因为你必须实现一个清晰的接口(通常由多个方法组成),而B有点无政府主义(这意味着它的灵活性).

核心

  1. 这些范式是否有任何一般结果/比较研究?
  2. 加速是否显着?
  3. 编译时间怎么样?
  4. 对于大型系统中的接口,有哪些设计含义(我主要用于A我的模块间接口,到目前为止我还没有做过非常大的项目)?

编辑

注意:说"动态绑定更好,因为它更强大"根本不是一个答案,因为前提条件是你有两种方法适用的情况(否则没有选择的自由 - 至少不合理) .

jus*_*tin 6

这些范式是否有任何一般结果/比较研究?

从我所看到的,许多证据的例子可以在文章和出版物中找到.你最喜欢的c ++书应该提供几个演示; 如果你没有这样的资源,你可能想阅读现代C++设计:应用的通用编程和设计模式 - A. Alexandrescu.虽然,没有一个特定的资源可以直接回答你的问题.同样,结果会因实现和编译器而异 - 甚至编译器设置也会极大地影响这种测试的结果.(回答你的每个问题,虽然这不符合这个具体问题的答案).

加速是否显着?

简短回答:这取决于.

在您的示例中,编译器实际上可以使用静态分派甚至内联虚函数调用(编译器可以看到足够的信息).我现在要将响应从一个简单的例子(特别是OP)转移到更大,更复杂的程序.

扩展'依赖':是的,加速可以从不可测量到巨大.你必须(并且可能已经)意识到编译器可以通过泛型在编译时提供大量的信息.然后,它可以使用此信息更准确地优化您的程序.这方面的一个很好的例子是使用std::arrayVS std::vector.向量在运行时增加了灵活性,但成本可能非常高.向量需要实现更多的调整大小,动态分配的需求可能是昂贵的.还有其他不同之处:数组的后备分配不会改变(++优化),元素数量是固定的(++优化),而且在很多情况下都没有必要调用new.

你现在可能会认为这个例子明显偏离了原来的问题.在很多方面,它实际上并没有那么不同:随着复杂程度的扩展,编译器对程序的了解越来越多.这个信息可以删除程序的几个部分(死代码),并使用std::array类型提供的信息就足够了,编译器可以很容易地说"哦,我看到这个数组的大小是七个元素,我将展开相应地循环"并且您将获得更少的指令并且将消除错误预测.还有更多内容,但在数组/矢量情况下,我看到优化程序的可执行文件大小在转换vector为类似的界面时减少到20%array.同样,代码可以执行几倍的速度.事实上,一些表达式可以完全在编译时计算.

动态调度仍然有其优点,如果正确使用动态调度也可以提高程序的速度 - 你真正需要学习的内容归结为决定何时优先于另一个.类似于具有许多变量的巨大函数无法非常有效地优化(实际程序中所有模板扩展的结果),在许多情况下,虚拟函数调用实际上可以是更快,更清晰的方法.因此,它们是两个独立的功能,你需要一些练习来确定什么是正确的(许多程序员不会花时间去学好这个).

总之,它们应被视为单独的特征,适用于不同的场景.这些应该(imho)的实际重叠程度远远低于他们在现实世界中的实际重叠程度.

编译时间怎么样?

使用模板,开发期间的编译和链接时间可能非常高.每次标题/模板更改时,您都需要对所有依赖项进行编译 - 这通常可以成为支持动态调度的重要福音.如果您提前计划并适当地构建,您当然可以减少这一点 - 了解如何通过模板掌握更难的主题.使用模板,您不仅可以增加大型构建的频率,还可以增加大型构建的时间和复杂性.(更多说明如下)

对于大型系统中的接口,有哪些设计含义(我主要使用A作为我的模块间接口,到目前为止我还没有真正做过很大的项目)?

这真的取决于你的计划的期望.我virtual每年写的都少(还有很多其他的).在其他方法中,模板变得越来越普遍.老实说,我不明白B"无政府主义"是怎样的.对我来说,A有点不合时宜,因为有很多合适的选择.它最终是一个设计选择,可以很好地考虑很好地构建大型系统.一个好的系统将使用该语言功能的健康组合.历史证明在这个讨论中没有必要编写一个非平凡的程序,但是所有功能都被添加了,因为有些人在某些特定用途中看到了更好的选择.你还应该期望lambdas在一些(不是所有)团队/代码库中替换当前用途的50%以上的虚拟.

推广:

  • 如果正确使用,模板的执行速度会明显加快.
  • 或者可以产生更大的可执行文件 如果使用正确且可执行文件大小很重要,那么编写器将使用多种方法来减少可执行文件大小,同时提供良好的可用接口
  • 模板可以变得非常复杂.学习浏览并解释错误消息需要时间.
  • 模板将多个错误压入编译域.就个人而言,我赞成编译错误而不是运行时错误.
  • 通过虚拟减少编译时间通常很简单(虚拟属于.cpp).如果您的程序很大,那么经常更改的大型模板系统可以快速发送您的重建时间并计算在屋顶上,因为会有很多模块间可见性和依赖性.
  • 可以使用具有较少编译文件的延迟和/或选择性实例化来减少编译时间.
  • 在较大的系统中,您必须更加体贴地为您的团队/客户强制进行重要的重新编译.使用虚拟是一种最小化这种方法的方法.同样,将在cpp中定义更高百分比的方法.替代方案当然是您可以隐藏更多的实现,或者为客户提供更具表现力的方式来使用您的接口.
  • 在较大的系统中,模板,lambda/functor等实际上可用于显着减少耦合和依赖性.
  • 虚拟增加依赖性,通常变得难以维护,膨胀界面,并成为结构上笨拙的野兽.以模板为中心的库倾向于颠倒该优先级.
  • 所有方法都可以用于错误的原因.

底线 一个设计精良的大型现代系统将有效地同时使用许多范例.如果你目前大部分时间都在使用虚拟机,那么你(错误地)做错了 - 特别是如果你有时间吸收c ++ 11那么这仍然是方法.如果速度,性能和/或并行性也是重要问题,那么模板和lambdas应该成为你们的亲密朋友.