【问题标题】:Why does the Rust compiler not reuse the memory on the stack after an object is moved?为什么在移动对象后,Rust 编译器不重用堆栈上的内存?
【发布时间】:2021-08-02 13:35:43
【问题描述】:

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

#[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. 这是一个有效的优化,但暂时关闭,如this
  4. 这是一个有效的优化,但被遗漏了。将来会添加。

【问题讨论】:

  • 因为你使用它的地址
  • 如链接问题中答案的rest所述,由LLVM选择是否为内存中的不同对象重用地址空间,并观察地址堆栈中的值会影响编译输出。 Rust 编译器本身不会强加一种行为。
  • @trentcl 我将再次反对打印应该放弃优化的想法:获取引用在 rust 中绝对无处不在,大多数方法调用都会这样做。如果这足以deopt那么我们就有问题(如果不是一个大问题)。尽管 Emoun 下面的调查似乎暗示问题可能出在其他地方。
  • @Masklinn 你错了。单独获取引用不会抑制优化,因为对象的地址对代码的行为没有任何可观察到的影响。直接打印或以其他方式观察对象的地址确实会抑制优化,因为优化器必须使用非本地推理来得出允许打印“任何”值的结论。
  • @trentcl 我刚刚创建了另一个示例。 godbolt.org/z/TEY76Wsjj 在此示例中,(1) .push_str() 强制 String 实例占用堆栈上的空间 (2) 不会改变任何可观察的行为,因为没有任何东西是可观察的 (3) 在 s 结束其生命周期后不会重用空间

标签: rust llvm compiler-optimization


【解决方案1】:

我的 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 使用的内存位置标记为空闲。正如 cmets 中提到的,实际处理这种优化的是 LLVM(据我所知称为'Stack Coloring'),这意味着当内存不再使用时,rustc 必须正确地告诉它。显然,从您的上一个示例中, rustc 在范围退出时执行此操作,但显然不是在移动对象时执行此操作。

【讨论】:

  • 我不会称其为明显的优化,更重要的是您希望使用尽可能少的堆栈而不是不使用。无论如何,代码运行速度相同。
  • @Stargateur 如果考虑缓存,较小的内存占用会提供更好的局部性和更少的缓存未命中,因此代码将运行得更快。此外,在嵌入式系统上,RAM 很少见,优化可以产生影响。
  • 在这种特定情况下,它可能并不重要,使用更多/更少堆栈可能会影响现实世界中的性能,因此更大的函数可以从优化中受益。另外,这个优化是enabled in LLVM for -O0 and above,所以他们清楚地认为它几乎总是值得的。
  • 驱逐缓存行可能会导致其他数据读取/写入丢失。例如。该函数可以驱逐其调用者使用的缓存行,这意味着调用者稍后可能会丢失。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-05
    • 2021-01-08
    • 1970-01-01
    • 2016-02-27
    • 1970-01-01
    • 2020-04-25
    • 2020-10-27
    相关资源
    最近更新 更多