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

2023-12-30

虽然动态绑定和模板的核心本质上是不同的东西,但它们可以用来实现相同的功能。

代码示例(仅供参考)

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());
  }
}

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
  }
}

Question

一些想法

虽然这些范式并不相同并且各有优缺点(A有点强大(参见MyCode2) and B对于用户来说更灵活)它们都允许实现相同的功能(尽管上面暗示的限制适用)。

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

Core

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

Edit

注意:说“动态绑定更好,因为它更强大”根本不是答案,因为前提是你有两种方法都适用的情况(否则就没有选择的自由——至少不合理) 。


这些范式有什么一般结果/比较研究吗?

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

提速是否显着?

简短的回答:这取决于。

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

扩展“这取决于”:是的,加速的范围可以从无法测量到巨大。您必须(并且可能已经)意识到编译器可以在编译时通过泛型提供令人难以置信的大量信息。然后它可以使用这些信息来更准确地优化您的程序。一个很好的例子是使用std::array vs std::vector。向量增加了运行时的灵活性,但成本可能相当大。向量需要实现更多的调整大小,动态分配的需要可能会很昂贵。还有其他区别:数组的后备分配不会改变(++优化),元素计数是固定的(++优化),并且同样 - 在许多情况下不需要通过 new 进行调用。

您现在可能认为这个例子与原来的问题有很大的偏差。在很多方面,它实际上并没有那么不同:随着程序复杂性的增加,编译器对程序的了解越来越多。此信息可以删除程序的几个部分(死代码)并使用std::array例如,类型提供的信息足够了,编译器可以轻松地说“哦,我看到这个数组的大小是七个元素,我将相应地展开循环”,并且您将拥有更少的指令并消除错误预测。还有更多内容,但在数组/向量情况下,我发现优化程序的可执行大小在从vector到类似的界面array。此外,代码的执行速度可以提高数倍。事实上,有些表达式可以完全在编译时计算。

动态调度仍然有其优点,如果使用得当,使用动态调度还可以提高程序的速度 - 您真正需要学习的内容归结为决定何时优先选择其中之一。类似于无法非常有效地优化具有许多变量的大型函数(实际程序中所有模板扩展的结果),虚拟函数调用实际上在许多情况下是一种更快、更简洁的方法。因此,它们是两个独立的功能,您需要一些练习来确定什么是正确的(许多程序员没有花时间来充分学习这一点)。

总之,它们应该被视为独立的功能,适用于不同的场景。这些(恕我直言)应该比现实世界中的实际重叠少得多。

编译时间怎么样?

与模板,编译和link开发过程中的时间可能会相当长。每次标头/模板更改时,您都需要对所有依赖项进行编译——这通常对于支持动态分派来说是一个重大的好处。如果您提前计划并适当构建,您当然可以减少这种情况 - 理解how使用模板是一个更难掌握的主题。使用模板,您不仅会增加大型构建的频率,而且通常会增加大型构建的时间和复杂性。 (更多注释如下)

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

这实际上取决于您的程序的期望。我写的virtual每年都会减少(还有许多其他情况)。除其他方法外,模板正变得越来越常见。老实说,我不明白怎么办B是“无政府主义”。大部头书,A有点不合时宜,因为有很多合适的替代品。它最终是一种设计选择,需要充分考虑才能很好地构建大型系统。一个好的系统将使用语言功能的健康组合。历史证明,本次讨论中的任何功能都不是编写一个不平凡的程序所必需的,但所有功能都被添加了,因为有人在某些特定用途中看到了更好的替代方案。您还应该期望 lambda 能够在某些(不是all) 团队/代码库。

概括:

  • 如果使用得当,模板的执行速度会显着加快。
  • 两者都可以生成更大的可执行文件。如果使用正确并且可执行文件大小很重要,那么作者将使用多种方法来减少可执行文件大小,同时提供良好的可用接口。
  • 模板可能会变得非常非常复杂。学习爬行并解释错误消息需要时间。
  • 模板将多个错误推入编译域。就我个人而言,我更喜欢编译错误而不是运行时错误。
  • 通过虚拟减少编译时间通常很简单(虚拟属于 .cpp)。如果您的程序很大,那么经常更改的巨大模板系统可以快速使您的重建时间和计数达到极限,因为会有大量的模块间可见性和依赖性。
  • 可以使用具有较少编译文件的延迟和/或选择性实例化来减少编译时间。
  • 在较大的系统中,您必须更加考虑强制为您的团队/客户进行重要的重新编译。使用虚拟的是one方法来最小化这种情况。同样,更高比例的方法将在 cpp 中定义。当然,另一种选择是您可以隐藏更多的实现或向客户端提供更具表现力的方式来使用您的接口。
  • 在较大的系统中,模板、lambdas/函子等实际上可以用来显着减少耦合和依赖。
  • 虚拟增加了依赖性,通常变得难以维护,界面膨胀,并成为结构上笨拙的野兽。以模板为中心的库往往会颠倒这种优先顺序。
  • 所有方法都可能因错误的原因而被使用。

底线一个大型的、设计良好的现代系统将使用many有效且同时的范式。如果你目前大部分时间都使用虚拟,那么你(在我看来)做错了——特别是如果你有时间吸收 c++11 后仍然采用这种方法。如果速度、性能和/或并行性也是重要问题,那么模板和 lambda 值得成为您的亲密朋友。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

接口范例性能(动态绑定与泛型编程) 的相关文章

随机推荐