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

anj*_*ruu 2 rust

假设我在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 }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后我有以下代码片段:

let a = Num::new(0);
let b = Num::new(1);
let c = a + b;
let d = a + b;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

She*_*ter 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());
}
Run Code Online (Sandbox Code Playgroud)

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

发布模式 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
}
Run Code Online (Sandbox Code Playgroud)

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

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

#[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)));
}
Run Code Online (Sandbox Code Playgroud)

产生

; 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
}
Run Code Online (Sandbox Code Playgroud)

哎呀,编译器看到我们基本上做了(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)));
}
Run Code Online (Sandbox Code Playgroud)

产生

; 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
}
Run Code Online (Sandbox Code Playgroud)

一个简单的add指令.

真正的结果是:

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

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

如您所见,Rust编译器 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Vee*_*rac 6

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

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

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

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

我已经在其他地方更详细地谈到了这一点.

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