Rust 是否去虚拟化特征对象函数调用?

Does Rust devirtualize trait object function calls?

devirtualize: to change a virtual/polymorphic/indirect function call into a static function call due to some guarantee that the change is correct -- source: myself

给定一个简单的特征对象,&dyn ToString,使用静态已知类型创建,String:

fn main() {
    let name: &dyn ToString = &String::from("Steve");
    println!("{}", name.to_string());
}

.to_string()的调用是否直接使用<String as ToString>::to_string()?或者只是通过特征的 vtable 间接地?如果是间接的,是否可以将此调用去虚拟化?还是有什么基本因素阻碍了这种优化?

这道题的激发代码要复杂得多;它使用异步特征函数,我想知道在某些情况下是否可以优化返回 Box<dyn Future>

Does Rust devirtualize trait object function calls?

没有

Rust 是一种语言,它不做任何事情;它只规定了语义。

在这种特定情况下,Rust 语言没有规定去虚拟化,因此允许实现去虚拟化。


目前,唯一稳定的实现是带有 LLVM 后端的 rustc——尽管如果您喜欢冒险,也可以使用 cranelift 后端。

您可以在 the playground 和 select“显示 LLVM IR”而不是“运行”以及“发布”而不是“调试”上测试此实现的代码",你应该可以检查没有虚拟调用。

代码的修订版本将强制转换隔离为 trait + 动态调用以使其更容易:

#[inline(never)]
fn to_string(s: &String) -> String {
    let name: &dyn ToString = s;
    name.to_string()
}

fn main() {
    let name = String::from("Steve");
    let name = to_string(&name);
    println!("{}", name);
}

the playground 上的 运行 产生其他东西时:

; playground::to_string
; Function Attrs: noinline nonlazybind uwtable
define internal fastcc void @_ZN10playground9to_string17h4a25abbd46fc29d4E(%"std::string::String"* noalias nocapture dereferenceable(24) %0, %"std::string::String"* noalias readonly align 8 dereferenceable(24) %s) unnamed_addr #0 {
start:
; call <alloc::string::String as core::clone::Clone>::clone
  tail call void @"_ZN60_$LT$alloc..string..String$u20$as$u20$core..clone..Clone$GTclone17h1e3037d7443348baE"(%"std::string::String"* noalias nocapture nonnull sret dereferenceable(24) %0, %"std::string::String"* noalias nonnull readonly align 8 dereferenceable(24) %s)
  ret void
}

你可以清楚地看到对 ToString::to_string 的调用已被对 <String as Clone>::clone 的简单调用所取代;去虚拟化调用。

The motivating code for this question is much more complicated; it uses async trait functions and I'm wondering if returning a Box<dyn Future> can be optimized in some cases.

很遗憾,您不能从上面的例子中得出任何结论。

优化很挑剔。本质上,大多数优化类似于模式匹配+使用正则表达式替换:人类看起来良性的差异可能会完全抛弃模式匹配并阻止优化应用。

如果重要的话,唯一可以确定在您的情况下应用了优化的方法是检查发出的程序集。

但是,实际上,在这种情况下,与虚拟调用相比,我更担心内存分配。虚拟调用大约有 5ns 的开销——尽管它确实抑制了一些优化——而内存分配(和最终的释放)通常花费 20ns - 30ns。

Does the call to .to_string() use <String as ToString>::to_string() directly? Or only indirectly via the trait's vtable?

我们可以通过编写两个函数来测试这种情况,一个使用 dyn ToString,一个直接使用具体类型 String

pub fn dyn_to_string() {
    let name: &dyn ToString = &String::from("Steve");
    println!("{}", name.to_string());
}

pub fn concrete_to_string() {
    let name: &String = &String::from("Steve");
    println!("{}", name.to_string());
}

现在我们可以查看生成的程序集:

playground::dyn_to_string:
    ...
    callq   *<alloc::string::String as core::clone::Clone>::clone@GOTPCREL(%rip)
    movq    %rbx, 24(%rsp)
    leaq    <alloc::string::String as core::fmt::Display>::fmt(%rip), %rax

如您所见,dyn_to_string 已优化为直接使用 <String as Clone>::clone 而不是通过 vtable 间接使用 - 它已被去虚拟化。其实具体实现和trait对象调用完全一样:

set playground::concrete_to_string, playground::dyn_to_string

但是,要回答更广泛的问题:

Does Rust devirtualize trait object function calls?

视情况而定。编译器不能总是执行去虚拟化。它在上面的代码中做到了,但在其他情况下,它可能不会。您不应该期望特征对象调用会被去虚拟化。泛型是有保证的零成本抽象。特征对象不是。