【问题标题】:What are the actual runtime performance costs of dynamic dispatch?动态调度的实际运行时性能成本是多少?
【发布时间】:2015-04-21 17:15:06
【问题描述】:

static and dynamic dispatch 的 Rust 书籍部分有一些关于这个主题的背景,但 tl;dr 是在 trait 引用和其他一些不同情况(函数指针等)上调用方法会导致动态而不是静态调度。

在应用优化后,它的实际运行时成本是多少?

例如,想象一下这组结构和特征:

struct Buffer;
struct TmpBuffer;
struct TmpMutBuffer;

impl BufferType for Buffer { ... }
impl BufferType for BufferTmp { ... }
impl BufferType for BufferTmpMut { ... }

impl Buffer2D for BufferType { ... }

impl Buffer2DExt for Buffer2D { ... }

请注意,这里的特征是在特征本身上实现的。

Buffer2DExt 在结构引用上调用方法的动态调度调用成本是多少?

最近的问题What are Rust's exact auto-dereferencing rules?关于解引用规则;这些规则是在编译时应用还是在运行时应用?

【问题讨论】:

  • 在结构引用上调用任何方法将是静态调度,而不是动态调度,因为您已经知道类型以及要调用的确切函数。
  • 解引用规则在编译时应用。

标签: rust


【解决方案1】:

免责声明:这个问题是相当开放的,因此这个答案可能不完整。用比平时更大的盐来处理它。

Rust 使用一个简单的“虚拟表”来实现动态调度。此策略也用于 C++ 中 you can see a study here。不过这项研究有点过时了。

间接成本

虚拟调度导致间接,这有多种原因:

  • 间接是不透明的:这会抑制内联和常量传播,这是许多编译器优化的关键推动因素
  • 间接具有运行时成本:如果预测不正确,您将看到管道停顿和昂贵的内存提取

优化间接

然而,编译器会尽最大努力优化间接性,从而把事情搞得一团糟。

  • 去虚拟化:有时编译器可以在编译时解析虚拟表查找(通常是因为它知道对象的具体类型);如果是这样,它因此可以使用常规函数调用而不是间接函数调用,并优化掉间接函数
  • 概率去虚拟化:去年Honza Hubička introduced a new optimization in gcc(阅读 5 部分系列,因为它很有启发性)。该策略的要点是构建继承图以对潜在类型进行有根据的猜测,然后使用if v.hasType(A) { v.A::call() } elif v.hasType(B) { v.B::call() } else { v.virtual-call() } 之类的模式;在这种情况下,对最可能的类型进行特殊处理意味着 常规 调用,因此是内联/常量传播/完整的调用。

由于一致性规则和隐私规则,后一种策略在 Rust 中可能相当有趣,因为它应该有更多的情况可以证明完整的“继承”图是已知的。

单态化的代价

在 Rust 中,您可以使用编译时多态性而不是运行时多态性;编译器将为它采用的每个编译时参数的唯一组合发出一个版本的函数。这本身就是有代价的:

  • 编译时间成本:要生成更多代码,需要优化更多代码
  • 二进制文件大小成本:生成的二进制文件最终会变得更大,这是一个典型的大小/速度权衡
  • 运行时成本:较大的代码大小可能会导致 CPU 级别的缓存未命中

编译器可能能够将最终具有相同实现的专用函数合并在一起(例如,由于幻像类型),但它仍然很可能比生成的二进制文件(可执行文件和库)最终更大.

与通常的性能一样,您必须根据自己的情况衡量什么更有益。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-27
    • 2011-02-12
    • 2015-05-25
    • 1970-01-01
    • 2010-09-20
    • 2012-12-26
    • 1970-01-01
    • 2016-08-11
    相关资源
    最近更新 更多