为什么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
String occupies 24 bytes on the stack.
s at 0x7ffee3b011b0
r at 0x7ffee3b011c8

很明显,即使s被移动,r也不会重用最初属于 的堆栈上的 24 字节块s。我认为重用移动对象的堆栈内存是安全的,但为什么 Rust 编译器不这样做呢?我错过了任何角落案例吗?

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

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

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

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

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

删除drop不影响结果。

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

为什么这样做?因为 Rust 语义规定一个对象在其范围的末尾被删除。由于s在内部作用域中,因此在作用域退出之前将其删除。如果没有作用域,s则在main函数退出之前一直处于活动状态。

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

  • @Stargateur If you take cache into account, smaller memory footprint gives better locality and less cache miss, so the code will run faster. Also, on embedded systems, RAM is rare, the optimization can make a difference.
  • In this specific case it probably doesn't matter much, using more/less stack is likely affect performance in the real-world so larger functions could benefit from optimization as this. Also, this optimization is [enabled in LLVM for -O0 and above](https://llvm.org/doxygen/TargetPassConfig_8cpp_source.html#l01088), so they have clearly decided it is almost always worth it.
  • Evicting a cache line might cause other data reads/writes to miss. E.g. the function could evict a cache line used by its caller, which means the caller might later get a miss.
  • I filed https://github.com/rust-lang/rust/issues/85230 about this.

以上是为什么Rust编译器在移动对象后不重用堆栈上的内存?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>