首页 文章

Rust中的复制语义字面上会成为内存中的副本吗?

提问于
浏览
2

假设我在Rust中有以下结构:

struct Num {
  pub num: i32;
}

impl Num {
  pub fn new(x: i32) -> Num {
    Num { num: x }
  }
}

impl Clone for Num {
  fn clone(&self) -> Num {
    Num { num: self.num }
  }
}

impl Copy for Num { }

impl Add<Num> for Num {
  type Output = Num;
  fn add(self, rhs: Num) -> Num {
    Num { num: self.num + rhs.num }
  }
}

然后我有以下代码片段:

let a = Num::new(0);
let b = Num::new(1);
let c = a + b;
let d = a + b;

这是有效的,因为 Num 被标记为 Copy . 否则,第二次添加将是编译错误,因为在第一次添加期间 ab 已经被移动到 add 函数中(我认为) .

问题是发射的组件的作用 . 当调用 add 函数时,是否有两个参数副本进入新的堆栈框架,或者Rust编译器是否足够聪明,知道在这种情况下,没有必要进行复制?

如果Rust编译器不够智能,并且实际上像C语言中的值传递的函数一样进行复制,那么在重要的情况下如何避免性能开销?

上下文是我正在实现一个矩阵类(只是为了学习),如果我有一个100x100矩阵,我真的不想每次尝试进行乘法或加法时调用两个副本 .

2 回答

  • 6

    问题是发射的组件的作用

    没有必要猜测;你可以看看 . 我们来使用这段代码:

    use std::ops::Add;
    
    #[derive(Copy, Clone, Debug)]
    struct Num(i32);
    
    impl Add for Num {
        type Output = Num;
    
        fn add(self, rhs: Num) -> Num {
            Num(self.0 + rhs.0)
        }
    }
    
    #[inline(never)]
    fn example() -> Num {
        let a = Num(0);
        let b = Num(1);
        let c = a + b;
        let d = a + b;
        c + d
    }
    
    fn main() {
        println!("{:?}", example());
    }
    

    将其粘贴到Rust Playground中,然后选择Release模式并查看LLVM IR:

    release mode

    LLVM IR

    搜索结果以查看 example 函数的定义:

    ; playground::example
    ; Function Attrs: noinline norecurse nounwind nonlazybind readnone uwtable
    define internal fastcc i32 @_ZN10playground7example17h60e923840d8c0cd0E() unnamed_addr #2 {
    start:
      ret i32 2
    }
    

    这是正确的,这在编译时完全和完全评估,并简化为一个简单的常量 . 编译器现在很不错 .

    也许你想尝试一些不像硬编码的东西?

    #[inline(never)]
    fn example(a: Num, b: Num) -> Num {
        let c = a + b;
        let d = a + b;
        c + d
    }
    
    fn main() {
        let something = std::env::args().count();
        println!("{:?}", example(Num(something as i32), Num(1)));
    }
    

    产生

    ; playground::example
    ; Function Attrs: noinline norecurse nounwind nonlazybind readnone uwtable
    define internal fastcc i32 @_ZN10playground7example17h73d4138fe5e9856fE(i32 %a) unnamed_addr #3 {
    start:
      %0 = shl i32 %a, 1
      %1 = add i32 %0, 2
      ret i32 %1
    }
    

    哎呀,编译器看到我们基本上做了(x 1)* 2,所以它在这里进行了一些棘手的优化以获得2x 2.让我们更努力地尝试一下......

    #[inline(never)]
    fn example(a: Num, b: Num) -> Num {
        a + b
    }
    
    fn main() {
        let something = std::env::args().count() as i32;
        let another = std::env::vars().count() as i32;
        println!("{:?}", example(Num(something), Num(another)));
    }
    

    产生

    ; playground::example
    ; Function Attrs: noinline norecurse nounwind nonlazybind readnone uwtable
    define internal fastcc i32 @_ZN10playground7example17h73d4138fe5e9856fE(i32 %a, i32 %b) unnamed_addr #3 {
    start:
      %0 = add i32 %b, %a
      ret i32 %0
    }
    

    一个简单的 add 指令 .

    真正的结果是:

    • 查看为您的案例生成的程序集 . 即使是看似相似的代码也可能有不同的优

    • 执行微观和宏观基准测试 . 你永远不知道代码将如何在大局中发挥作用 . 也许您的所有缓存都会被烧毁,但您的微基准测试将会非常出色 .

    Rust编译器是否足够聪明,知道在这种情况下,没有必要进行复制?

    如您所见,Rust编译器 plus LLVM非常智能 . 通常,当知道不需要操作数时,可以删除副本 . 是否适用于您的情况很难回答 .

    即使它确实如此,您可能也不希望通过堆栈传递大型项目,因为它总是可能需要复制 .

    请注意,您不必为该值实现副本,您可以选择仅通过引用允许它:

    impl<'a, 'b> Add<&'b Num> for &'a Num {
        type Output = Num;
    
        fn add(self, rhs: &'b Num) -> Num {
            Num(self.0 + rhs.0)
        }
    }
    

    实际上,您可能希望实现添加它们的两种方式,也可能是所有4种值/引用的排列!

  • 13

    如果Rust编译器不够智能,并且实际上像C语言中的值传递的函数一样进行复制,那么在重要的情况下如何避免性能开销?上下文是我正在实现一个矩阵类(只是为了学习),如果我有一个100x100矩阵,我真的不想每次尝试进行乘法或加法时调用两个副本 .

    Rust的隐式副本(来自移动或实际的 Copy 类型)的 Allshallow memcpy . 如果堆分配,则只复制指针等 . 与C不同,按值传递矢量只会复制三个指针大小的值 .

    要复制堆内存,必须通过调用 .clone() ,使用 #[derive(Clone)]impl Clone 实现显式复制 .

    I've talked in more detail about this elsewhere.

    Shepmaster指出,浅拷贝经常被编译器弄乱 - 通常只有堆内存和大量堆栈值会导致问题 .

相关问题