为什么 Rust 中的 "move" 实际上并没有移动?

13
在下面的示例中:
struct Foo {
    a: [u64; 100000],
}

fn foo(mut f: Foo) -> Foo {
    f.a[0] = 99999;
    f.a[1] = 99999;
    println!("{:?}", &mut f as *mut Foo);

    for i in 0..f.a[0] {
        f.a[i as usize] = 21444;
    }

    return f;
}
fn main(){
    let mut f = Foo {
        a:[0;100000]
    };

    println!("{:?}", &mut f as *mut Foo);
    f = foo(f);
    println!("{:?}", &mut f as *mut Foo);
}

我发现在进入函数foo之前和之后,f的地址不同。为什么Rust会复制如此大的结构体但实际上并没有移动它(或实现这种优化)?

我了解堆栈内存的工作原理。但是通过Rust中的所有权提供的信息,我认为可以避免复制。编译器不必要地将数组复制两次。这可以成为Rust编译器的一种优化吗?


2
C++本质上是一样的。巨大的数据结构可以在不在函数或方法声明中使用&指示要传递引用的情况下转储到堆栈上。(在我的情况下,这是一个错误,在嵌入式系统中,有200K被转储到了16K的堆栈中。由于没有内存保护,几个其他堆栈也被擦除,系统很快就在无关代码中崩溃了。花了我几个小时找到单个缺失的&) - starblue
@starblue 有没有 & 可以有很大的区别。传递变量的引用将共享相同的内存。但是没有 &,将使用复制构造函数(或简单复制)来创建参数变量(它与传递后的变量没有关联)。但是,在 C++ 中使用 &&std::move 时会使用 move 来触发移动构造函数。在被“移动”到函数中之后,该变量将无法使用。因此,C++ 中的移动几乎具有与 Rust 相同的语义(没有所有权系统提供的安全性),但性能不同。 - YangKeao
1
你如何在C++移动构造函数中移动数组?对于大的东西使用盒子。 - Stargateur
1个回答

12

移动操作是一个memcpy操作,其后将源视为不存在。

你的大型数组存储在栈上。这就是Rust内存模型的工作方式:本地变量存储在栈上。因为函数返回时foo的堆栈空间也随之消失,所以编译器除了将内存复制到main的堆栈空间中外,别无选择。

在某些情况下,编译器可以重新排列代码,使得移动操作可以省略(源和目标合并成一个),但这种优化是不能依赖的,特别是对于大对象而言。

如果你不想在程序中复制巨大的数组,可以自己在堆上分配内存,可以通过Box<[u64]>或者简单地使用Vec<u64>来实现。


6
在这种情况下,您可以将函数传递给 &mut f ,并返回空值,这在惯用语中是合适的。 - starblue
2
在这种情况下,实际上可以避免移动。变量f是在main()的堆栈帧中创建的,编译器可以静态确定不需要将其移动到foo()的堆栈帧中,因为它最终会被复制回其原始位置。但即使在标记为#[inline(always)]foo()的发布版本中,编译器仍然会不必要地复制数组两次。 - Sven Marnach
1
我理解它如何工作,但所有权提供了更多信息给编译器,有了这些信息,我们可以在没有任何安全问题的情况下使用相同的内存部分。我认为这在某些情况下是一项重大优化。但Rust还没有做到这一点(但实际上当函数被内联时,它将使用相同的内存而不进行复制)。 - YangKeao
1
@YangKeao 即使将函数内联,复制似乎仍未被省略,但也许这是因为取地址的缘故,如果我们删除 println 调用,就不会发生这种情况了吗?到目前为止,我的理解是 Rust 应该能够优化这种情况。 - Sven Marnach
2
@YangKeao 我猜这些优化并不是作为保证存在的。如果你想保证没有发生复制,那么传递盒子的引用即可。 - Sven Marnach
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接