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

时间:2015-09-19 02:20:30

标签: rust

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

subprocess.PIPE

然后我有以下代码片段:

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。否则,第二次添加将是编译错误,因为Copya在第一次添加期间已经被移动到b函数中(我认为)。

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

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

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

2 个答案:

答案 0 :(得分:13)

  

问题是发出的组件的作用

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

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,然后选择发布模式并查看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指令。

真正的结果是:

  1. 查看为您的案例生成的程序集。即使是看起来相似的代码也可能有不同的优化。
  2. 执行微观和宏观基准测试。你永远不知道代码将如何在大局中发挥作用。也许您的所有缓存都会被烧毁,但您的微基准测试将会非常出色。
  3.   

    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种值/引用的排列!

答案 1 :(得分:6)

  

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

     

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

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

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

I've talked in more detail about this elsewhere.

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