为什么 Rust 编译器在移动对象后不重用堆栈上的内存?

Why does the Rust compiler not reuse the memory on the stack after an object is moved?

我以为一旦移动了一个对象,它在栈上占用的内存就可以重新用于其他目的。然而,下面的最小示例显示了相反的情况。

#[inline(never)]
fn consume_string(s: String) {
    drop(s);
}

fn main() {
    println!(
        "String occupies {} bytes on the stack.",
        std::mem::size_of::<String>()
    );

    let s = String::from("hello");
    println!("s at {:p}", &s);
    consume_string(s);

    let r = String::from("world");
    println!("r at {:p}", &r);
    consume_string(r);
}

使用 --release 标志编译代码后,它在我的计算机上给出了以下输出。

String occupies 24 bytes on the stack.
s at 0x7ffee3b011b0
r at 0x7ffee3b011c8

很明显,即使 s 被移动,r 也不会重用堆栈上原本属于 s 的 24 字节块。我想重用移动对象的堆栈内存是安全的,但为什么 Rust 编译器不这样做呢?我是否遗漏了任何极端情况?

更新: 如果我用大括号括起 sr 可以重用堆栈上的 24 字节块。

#[inline(never)]
fn consume_string(s: String) {
    drop(s);
}

fn main() {
    println!(
        "String occupies {} bytes on the stack.",
        std::mem::size_of::<String>()
    );

    {
        let s = String::from("hello");
        println!("s at {:p}", &s);
        consume_string(s);
    }

    let r = String::from("world");
    println!("r at {:p}", &r);
    consume_string(r);
}

上面的代码给出了下面的输出。

String occupies 24 bytes on the stack.
s at 0x7ffee2ca31f8
r at 0x7ffee2ca31f8

我认为花括号应该没有任何区别,因为 s 的生命周期在调用 comsume_string(s) 之后结束,并且它的删除处理程序在 comsume_string() 中被调用。为什么添加花括号可以优化?

我使用的 Rust 编译器版本如下。

rustc 1.54.0-nightly (5c0292654 2021-05-11)
binary: rustc
commit-hash: 5c029265465301fe9cb3960ce2a5da6c99b8dcf2
commit-date: 2021-05-11
host: x86_64-apple-darwin
release: 1.54.0-nightly
LLVM version: 12.0.1

更新 2: 我想澄清一下我对这个问题的关注点。我想知道提议的“堆栈重用优化”属于哪一类。

  1. 这是一个无效的优化。在某些情况下,如果我们执行“优化”,编译代码可能会失败。
  2. 这是一个有效的优化,但编译器(包括 rustc 前端和 llvm)无法执行它。
  3. 这是一个有效的优化,但暂时关闭,如
  4. 这是一个有效的优化,但被遗漏了。以后会补上的。

我的 TLDR 结论:错过了优化机会。

所以我做的第一件事就是研究您的 consume_string 函数是否真的有所作为。为此,我创建了以下(更多)最小示例:

struct Obj([u8; 8]);
fn main()
{
    println!(
        "Obj occupies {} bytes on the stack.",
        std::mem::size_of::<Obj>()
    );

    let s = Obj([1,2,3,4,5,6,7,8]);
    println!("{:p}", &s);
    std::mem::drop(s);
    
    let r = Obj([11,12,13,14,15,16,17,18]);
    println!("{:p}", &r);
    std::mem::drop(r);
}

我使用 std::mem::drop 而不是 consume_string,它专用于简单地使用一个对象。此代码的行为与您的一样:

Obj occupies 8 bytes on the stack.
0x7ffe81a43fa0
0x7ffe81a43fa8

删除 drop 不会影响结果。

所以问题是为什么 rustc 在 r 上线之前没有注意到 s 已经死了。正如您的第二个示例所示,将 s 包含在范围中将允许优化。

为什么这行得通?因为 Rust 语义规定对象在其范围的末尾被删除。由于 s 位于内部范围内,因此在范围退出之前将其删除。没有作用域,s 一直存在,直到 main 函数退出。

为什么将 s 移动到一个函数中却不起作用,它应该在退出时被删除? 可能是因为 rust 在函数调用后没有正确地将 s 使用的内存位置标记为空闲。正如评论中提到的,实际处理此优化的是 LLVM(据我所知称为 'Stack Coloring'),这意味着 rustc 必须在不再使用内存时正确地告诉它。显然,从你的最后一个例子来看,rustc 在范围退出时执行它,但显然不是在移动对象时执行。

我认为 fn drop 不会释放 S 的内存,只需调用 fn drop。 在第一种情况下,s 仍然使用堆栈内存,rust 不能被重用。 在第二种情况下,因为 {} 范围,内存是免费的。所以堆栈内存重用