Rust能够优化本地堆分配吗?

5

当编写相对实时的代码时,通常避免在主执行循环中进行堆分配。因此,在我的经验中,您需要在初始化步骤中分配程序所需的所有内存,然后根据需要传递内存。以C语言为例,一个玩具示例可能如下所示:

#include <stdlib.h>

#define LEN 100

void not_realtime() {
    int *v = malloc(LEN * sizeof *v);
    for (int i = 0; i < LEN; i++) {
        v[i] = 1;
    }
    free(v);
}

void realtime(int *v, int len) {
    for (int i = 0; i < len; i++) {
        v[i] = 1;
    }
}

int main(int argc, char **argv) {
    not_realtime();

    int *v = malloc(LEN * sizeof *v);
    realtime(v, LEN);
    free(v);
}

我相信在Rust中大致等价的代码:

fn possibly_realtime() {
    let mut v = vec![0; 100];
    for i in 0..v.len() {
        v[i] = 1;
    }
}

fn realtime(v: &mut Vec<i32>) {
    for i in 0..v.len() {
        v[i] = 1;
    }
}

fn main() {
    possibly_realtime();

    let mut v: Vec<i32> = vec![0; 100];
    realtime(&mut v);
}

我想知道的是:Rust是否能够优化possibly_realtime,使得对v的本地堆分配仅在第一次调用possibly_realtime时发生,并在后续调用中重复使用?我猜不行,但也许有一些神奇的方法可以实现。

我想指出Rust是一种语言,所以我们在谈论的是rustc(唯一可用的编译器),更具体地说,是rustc背后的LLVM后端(因为rustc将优化委托给LLVM)。 - Matthieu M.
2个回答

6
为了调查这个问题,将#[inline(never)]添加到您的函数中是很有用的,然后查看Playground上的LLVM IR

Rust 1.54

这里并没有进行优化。以下是摘录:

; playground::possibly_realtime
; Function Attrs: noinline nonlazybind uwtable
define internal fastcc void @_ZN10playground17possibly_realtime17h2ab726cd567363f3E() unnamed_addr #0 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
start:
  %0 = tail call i8* @__rust_alloc_zeroed(i64 400, i64 4) #9, !noalias !8
  %1 = icmp eq i8* %0, null
  br i1 %1, label %bb20.i.i.i.i, label %vector.body

每次调用possibly_realtime时,都会通过__rust_alloc_zeroed分配内存。

Rust 1.0稍早的版本

这个过程没有被优化。以下是一段摘录:

; Function Attrs: noinline uwtable
define internal fastcc void @_ZN17possibly_realtime20h1a3a159dd4b50685eaaE() unnamed_addr #0 {
entry-block:
  %0 = tail call i8* @je_mallocx(i64 400, i32 0), !noalias !0
  %1 = icmp eq i8* %0, null
  br i1 %1, label %then-block-255-.i.i, label %normal-return2.i

每次调用possibly_realtime,都会通过je_mallocx进行内存分配。
编辑意见:
重复使用缓冲区是泄漏安全信息的好方法,我鼓励你尽可能避免。我相信你已经熟悉这些问题,但我想确保未来的搜索者注意到这一点。
我也怀疑这种“优化”会被添加到Rust中,特别是没有程序员明确选择的情况下。需要一个地方来存储分配的内存指针,但实际上并没有。这意味着它必须是全局或线程本地变量! Rust可以在没有线程的环境中运行,但全局变量仍将排除对此方法的递归调用。总的来说,我认为将缓冲区传递到方法中更加明确地说明会发生什么。
我还假设您的示例使用具有固定大小的Vec以进行演示,但如果您真正知道编译时的大小,则固定大小的数组可能是更好的选择。

1
相关的PR是https://github.com/rust-lang/rust/pull/22526。跟踪malloc优化的问题在https://github.com/rust-lang/rust/issues/22159。 - oli_obk
不,这实际上还将malloc -> use -> dealloc优化为堆栈分配。我认为它也会对dealloc -> malloc进行noop优化。 - oli_obk
@ker 我一定是漏了什么; 我没有看到任何jemalloc调用的删除 (http://is.gd/v1Zb0g)。 inline(never) 不应该影响一个函数内的优化,对吧? - Shepmaster
嗯...显然,即使是已经死亡的分配也不再起作用了:http://is.gd/LWzpqM - oli_obk
@ker 哎呀!你要提交一个问题吗? - Shepmaster
显示剩余4条评论

2
截至2021年,Rust能够优化掉堆分配并内联虚函数调用(playground):
fn old_adder(a: f64) -> Box<dyn Fn(f64)->f64> {
    Box::new(move |x| a + x)
}

#[inline(never)]
fn test() {
    let adder = old_adder(1.);
    assert_eq!(adder(1.), 2.);
}

fn main() {
    test();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接