#dynamic-dispatch #performance #nightly #closures #calls #fn #virtual

ffd

快速函数调度:提高Rust动态函数调用的性能

2个版本

0.1.0-alpha.12024年4月7日
0.1.0-alpha.02024年4月6日

#3 in #fn

MIT许可证

9KB
92

快速函数调度:提高Rust动态函数调用的性能

crates.io crates.io License actions-badge

一个安全、实用的工具包,用于高性能虚函数调用。

该库提供了对如Box<dyn Fn(...) -> _>等类型的替代方案,在各种场景下性能更优。

功能标志

  • nightly:为Fn实现Func,同时允许Func::new接受多参数闭包

为什么?

你经常会听到说Rust充满了零成本的抽象。

在精神上,这通常是正确的!Rust的许多花哨功能确实编译成与在C这样的“底层”语言中手动编写的机器代码非常接近的代码,差异微乎其微。

遗憾的是,函数调度是一个例外。

当看到像下面的这样的特性和相应的dyn强制转换时,Rust的策略是生成一个vtable

trait MyTrait {
    fn do_something(&self);
    fn do_something_else(&self, x: i32);
}

struct MyStruct { a: i32 }

impl MyTrait for MyStruct {
    fn do_something(&self) { println!("{}", self.a); }
    fn do_something_else(&self, a: i32) { println!("{a}"); }
}

这个vtable可能看起来像这样

struct MyTraitVtable {
    // `*const ()` represents the `&self` argument of `do_something`
    do_something: fn(*const ()),
    do_something_else: fn(*const (), i32),
}

static MYSTRUCT_MYTRAIT_VTABLE: MyTraitVtable = MyTraitVtable {
    do_something: MyStruct::do_something as fn(_),
    do_something_else: MyStruct::do_something_else as fn(_, _),
};

总的来说,这是一个合理的策略:当编译器看到&dyn MyTrait时,它会在内部将其表示为宽指针,类似于以下元组

(*const (), *const MyTraitVtable)

第一个字段表示数据的指针,&self。第二个字段是vtable,允许我们在运行时查找方法。

当在特性对象上调用方法时,编译器将生成代码,首先解引用vtable指针以找到vtable,然后选择与被调用的方法相对应的字段。这个字段是一个函数指针:因此我们现在可以使用数据指针作为其参数来调用这个函数指针。

这对大多数特性来说工作得很好。

遗憾的是,Rust在动态函数调用调度方面也采用了相同的策略:对于Rust来说,Fn特性就像任何其他特性一样出现。这显然是不必要的低效!Fn特性只有一个非常常用的方法,即Fn::call:为什么我们需要执行双重间接引用,在内存中跳跃两个位置,而我们可以直接将Fn::call函数指针作为指针元数据传递呢?更糟糕的是,这种双重间接引用会严重降低调用者和被调用者的代码生成效率,破坏寄存器状态,并需要进行不必要的堆栈操作。

99%的情况下,这种相对较小的低效微不足道。然而,确实存在某些情况,这种开销开始变得重要,这就是这个库存在的目的。

计划中的功能

  • 涵盖并发使用情况:SendSync函数
  • 涵盖更多的Fn特性:FnMutFnOnce等。
  • 不同的表示策略:是否在指针元数据中丢弃函数?

无运行时依赖

特性