动态调度的实际运行时性能成本是多少?

Dou*_*oug 6 rust

关于静态和动态调度的生锈书部分中有关于此主题的一些基本背景,但tldr基本上是:在特征引用上调用方法和其他一些不同的情况(函数指针等)导致动态而不是静态调度.

所以,问题:

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

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

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 { ... }
Run Code Online (Sandbox Code Playgroud)

特别注意,这里的特征是在特征本身上实现的.

在struct reference上从Buffer2DExt调用方法的动态调度的调用成本是多少?

最近有一个关于解除引用规则的问题这是什么是Rust的确切自动解除引用规则?; 这些规则是在编译时还是运行时应用的?

Mat*_* M. 18

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

Rust使用一个简单的"虚拟表"来实现动态调度.此策略也用于C++,您可以在此处查看研究.这项研究有点过时了.


间接的代价

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

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

优化间接

然而,编译器通过尽力优化间接性来淹没水.

  • devirtualization:有时编译器可以在编译时解析虚拟表查找(通常,因为它知道对象的具体类型); 如果是这样,它可以使用常规函数调用而不是间接函数调用,并优化间接
  • 概率虚拟化:去年HonzaHubička在gcc中引入了一个新的优化.他在这里写博客,你应该准备好这个5部分的系列,它非常有启发性.策略的要点是构建继承图以对潜在类型进行有根据的猜测,然后使用类似的模式if v.hasType(A) { v.A::call() } elif v.hasType(B) { v.B::call() } else { v.virtual-call() }; 特殊套管最可能的类型意味着在这种情况下的常规调用,因此内联/常量传播/全好的调用.

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


单形化(和内联)的成本

正如Shepmaster所推动的,替代品的成本.

在Rust中,您可以使用编译时多态而不是运行时多态; 编译器将为每个独特的编译时参数组合发出一个版本的函数.这本身就有成本:

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

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

像往常一样,你必须在你的情况下衡量什么是更有益的.