【问题标题】:What is the Rust equivalent of storing C++ std::function in a vector?将 C++ std::function 存储在向量中的 Rust 等价物是什么?
【发布时间】:2020-01-24 00:32:47
【问题描述】:

这是我想要实现的 C++ 等价物:

std::vector<std::function<int(int)>> funcs;
funcs.emplace_back([](int n) -> int { return n + 1; });
int result = funcs[0](33);

如何用 Rust 编写上面的代码?

【问题讨论】:

    标签: vector lambda rust closures


    【解决方案1】:

    如果您不打算将函数移动到任何特定位置,您可以让类型推断在代码块中为您完成工作,并从字面上定义您的闭包,就像它是一个普通变量一样(实际上,它是 - 它实现FnFnMut):

    let my_lambda = |n| n+1;
    println!("{}", my_lambda(33));
    

    Playground

    如果您打算将此 lambda 移出堆栈,则需要将其装箱:

    let my_lambda: Box<dyn Fn(u32) -> u32> = Box::new(|n| n + 1);
    println!("{}", my_lambda(33));
    

    Playground

    这里的原理保持不变,唯一真正的区别是 lambda 现在在堆上。

    将它们存储在Vec 中变得相对简单,因为到目前为止我们已经证明它们是“正常”类型。没有什么能阻止您创建闭包向量,但您需要将它们装箱(Vec 要求每个元素都是 Sized,并且无法通过签名来定位两个不同的闭包)并且它们需要具有相同的签名:

    let my_vector: Vec<Box<dyn Fn(u16) -> u16>> = vec![
        Box::new(|i| i + 1),
        Box::new(|i| i - 1),
    ];
    println!("{}", my_vector[0](33))
    

    如果它们有不同的签名,则需要编写自己的结构来包含它们并通过签名对它们进行存储,这不是一项简单的任务。


    基准案例:

    • C++:

      #include <vector>
      #include <iostream>
      #include <functional>
      int main(int argc, char **argv) {
        std::vector<std::function<int(int)>> funcs;
        for (int i = 0; i < 10000000; i++) {
          funcs.push_back([&] (int n) { return n + 1; });
          int result = funcs[i](33);
        }
      }
      
    • 生锈:

      fn main() {
         let mut lambdas:Vec<Box<Fn(u32) -> u32>> = vec![];
         for i in 0..10000000 {
           lambdas.push(Box::new(|i| i+1));
           lambdas[i](3);
         }
      }
      

    编译选项:

    • G++:-O3 --std=c++0x
    • 货运:--release

    结果:

    • C++ 峰值堆大小:

      --------------------------------------------------------------------------------
        n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
      --------------------------------------------------------------------------------
       68    383,911,899      268,512,208      268,508,160         4,048            0
       69    555,878,425      805,387,160      805,379,072         8,088            0
       70    765,593,697      805,387,160      805,379,072         8,088            0
      
    • Rust 峰值堆大小:

      --------------------------------------------------------------------------------
        n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
      --------------------------------------------------------------------------------
       46    210,486,321      268,440,920      268,436,765         4,155            0
       47    210,486,371      268,440,976      268,436,805         4,171            0
       48    210,486,496      268,441,064      268,436,885         4,179            0
      

    结论:

    • C++ std::function 包装器是堆分配的。它包含的原始 lambda 本身是堆栈分配的,这种行为与 Rust 一致
    • Rust 更擅长内存优化,占用 C++ 三分之一的堆大小
    • Rust 在原始时间方面也更快

    【讨论】:

    • 如果我理解正确,Box 将在堆上分配每个 lambda,所以这里我们在堆上分配了向量,每个元素也在堆上分配,与 C++ 相比, lambdas 不是堆分配的(只是向量本身)。是这样吗?
    • 比这复杂一点。 Vec 本身在堆栈上;它指向内容的内部指针在堆上;并且 lambdas 本身在堆上(毕竟它们是 Vec 的元素)。
    • 是的,我明白这一点。我的意思是 C++ 版本似乎更高效,因为它只进行 1 次分配:向量内容。 Rust 版本实际上分配了一个 pointers 的向量,然后它再次 分配堆中的每个 lambda,将指针存储在向量中。我就是这么看的。
    • “但你需要把它们装箱”——这通常不是真的。如果闭包不捕获任何局部变量,可以将它们强制为函数指针,您可以将它们直接存储在向量中:let v: Vec&lt;fn(i32) -&gt; i32&gt; = vec![|n| n + 1, |n| n - 1];.
    • 谁能分享获取基准数据的命令/方式?我想自己重现,我怀疑 C++ 的缓慢是因为 lambda 已被复制到向量中。所以在 C++ 中有更多的堆分配。
    猜你喜欢
    • 2021-02-08
    • 2021-03-11
    • 1970-01-01
    • 2017-04-10
    • 1970-01-01
    • 2021-10-23
    • 1970-01-01
    • 1970-01-01
    • 2014-05-08
    相关资源
    最近更新 更多