假设我在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
。否则,第二次添加将是编译错误,因为Copy
和a
在第一次添加期间已经被移动到b
函数中(我认为)。
问题是发射的组件的作用。当调用add
函数时,是否会将两个参数复制到新的堆栈框架中,或者Rust编译器是否足够聪明,知道在这种情况下,没有必要进行复制?
如果Rust编译器不够智能,并且实际上像C ++中通过值传递的参数一样进行复制,那么在重要的情况下如何避免性能开销呢?
上下文是我正在实现矩阵类(只是为了学习),如果我有一个100x100矩阵,我真的不想每次尝试乘法或添加时调用两个副本。
答案 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:
搜索结果以查看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种值/引用的排列!
答案 1 :(得分:6)
如果Rust编译器不够智能,并且实际上像C ++中通过值传递的参数一样进行复制,那么在重要的情况下如何避免性能开销呢?
上下文是我正在实现矩阵类(只是为了学习),如果我有一个100x100矩阵,我真的不想每次尝试乘法或添加时调用两个副本。
所有 Rust的隐式副本(来自移动或实际的Copy
类型)都是浅 memcpy
。如果堆分配,则只复制指针等。与C ++不同,按值传递矢量只会复制三个指针大小的值。
要复制堆内存,必须通过调用.clone()
,使用#[derive(Clone)]
或impl Clone
实现显式副本。
I've talked in more detail about this elsewhere.
Shepmaster指出,浅拷贝经常被编译器弄乱 - 通常只有堆内存和大量堆栈值会导致问题。