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

Zhi*_* Ma 14 llvm compiler-optimization 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);
}
Run Code Online (Sandbox Code Playgroud)

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

String occupies 24 bytes on the stack.
s at 0x7ffee3b011b0
r at 0x7ffee3b011c8
Run Code Online (Sandbox Code Playgroud)

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

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

String occupies 24 bytes on the stack.
s at 0x7ffee3b011b0
r at 0x7ffee3b011c8
Run Code Online (Sandbox Code Playgroud)

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

String occupies 24 bytes on the stack.
s at 0x7ffee2ca31f8
r at 0x7ffee2ca31f8
Run Code Online (Sandbox Code Playgroud)

我认为大括号应该没有任何区别,因为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
Run Code Online (Sandbox Code Playgroud)

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

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

Ema*_*oun 7

我的 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);
}
Run Code Online (Sandbox Code Playgroud)

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

Obj occupies 8 bytes on the stack.
0x7ffe81a43fa0
0x7ffe81a43fa8
Run Code Online (Sandbox Code Playgroud)

删除drop不影响结果。

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

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

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

  • @Stargateur如果考虑到缓存,较小的内存占用会提供更好的局部性和更少的缓存缺失,因此代码会运行得更快。此外,在嵌入式系统上,RAM 很少见,优化可以发挥作用。 (4认同)
  • 在这种特定情况下,这可能并不重要,使用更多/更少的堆栈可能会影响现实世界中的性能,因此更大的函数可以从优化中受益。此外,这种优化[在 LLVM 中为 -O0 及以上启用](https://llvm.org/doxygen/TargetPassConfig_8cpp_source.html#l01088),因此他们明确认为这几乎总是值得的。 (4认同)
  • 逐出缓存行可能会导致其他数据读/写丢失。例如,该函数可能会逐出其调用者使用的缓存行,这意味着调用者稍后可能会失败。 (2认同)
  • 我对此提交了 https://github.com/rust-lang/rust/issues/85230 。 (2认同)