首页 > 解决方案 > 为什么在移动对象后,Rust 编译器不重用堆栈上的内存?

问题描述

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

#[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. 我认为重用移动对象的堆栈内存是安全的,但为什么 Rust 编译器不这样做呢?我错过了任何角落案例吗?

更新:如果我s用大括号括起来,r可以重用堆栈上的 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. 这是一个有效的优化,但被遗漏了。将来会添加。

标签: rustllvmcompiler-optimization

解决方案


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

而不是consume_string我使用std::mem::dropwhich 专门用于简单地消费一个对象。这段代码的行为就像你的一样:

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

删除drop不影响结果。

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

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

为什么它在s进入一个函数时不起作用,它应该在退出时被丢弃?s可能是因为 rust在函数调用后没有正确地将所使用的内存位置标记为空闲。正如评论中提到的,实际上是 LLVM 处理了这种优化(据我所知称为“堆栈着色”),这意味着当内存不再使用时,rustc 必须正确地告诉它。显然,从您的上一个示例中, rustc 在范围退出时执行此操作,但显然不是在移动对象时执行此操作。


推荐阅读