2个版本
0.1.0-alpha.1 | 2024年4月7日 |
---|---|
0.1.0-alpha.0 | 2024年4月6日 |
#3 in #fn
9KB
92 行
快速函数调度:提高Rust动态函数调用的性能
一个安全、实用的工具包,用于高性能虚函数调用。
该库提供了对如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%的情况下,这种相对较小的低效微不足道。然而,确实存在某些情况,这种开销开始变得重要,这就是这个库存在的目的。
计划中的功能
- 涵盖并发使用情况:
Send
和Sync
函数 - 涵盖更多的
Fn
特性:FnMut
、FnOnce
等。 - 不同的表示策略:是否在指针元数据中丢弃函数?