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

2024-03-20

假设我在 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。否则,第二次添加将是一个编译错误,因为a and b已经被搬进了add在第一次添加期间起作用(我认为)。

问题是发出的程序集会做什么。当。。。的时候add函数被调用时,参数的两个副本是否被放入新的堆栈框架中,或者 Rust 编译器是否足够聪明,知道在这种情况下,没有必要进行复制?

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

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


问题是发出的程序集做什么

无需猜测;你可以看看。让我们使用这段代码:

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());
}

将其粘贴到铁锈游乐场 https://play.rust-lang.org/,然后选择Release模式并查看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)));
}

Produces

; 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)));
}

Produces

; 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. 执行微观和宏观基准测试。你永远不知道代码在大局中将如何发挥作用。也许你所有的缓存都会被耗尽,但你的微基准测试将会非常出色。

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

正如您刚才看到的,Rust 编译器plusLLVM 非常聪明。一般来说,是possible当它知道不需要操作数时删除副本。它是否适用于您的情况很难回答。

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Rust 中的复制语义真的会成为内存中的副本吗? 的相关文章

随机推荐