【问题标题】:Is Rust able to optimize local heap allocations?Rust 是否能够优化本地堆分配?
【发布时间】:2021-07-30 21:36:14
【问题描述】:

在编写相对实时的代码时,通常会避免主执行循环中的堆分配。因此,根据我的经验,您在初始化步骤中分配程序所需的所有内存,然后根据需要传递内存。 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)。

标签: rust


【解决方案1】:

要对此进行调查,请将#[inline(never)] 添加到您的函数中,然后查看LLVM IR on the playground

锈 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,但如果您在编译时确实知道大小,那么固定大小的数组可能是更好的选择。

【讨论】:

  • 太好了,谢谢。实际上,我的安全知识相当缺乏。很高兴知道。在我研究过的实时系统(主要是软件定义无线电)中,我们不太关心安全性,但性能至关重要。是的,固定尺寸用于演示。更现实的情况是来自配置文件的大小。
  • #[inline(never)] 不会破坏优化发生的可能性吗?大约一个月以来,Rust 进行了 malloc 优化......
  • @ker 我不知道 break,但可能会降低这种可能性。我已经编辑了答案,以提及为什么我认为这种“优化”不太可能存在。你能分享一些关于你正在考虑的优化的链接吗?
  • 相关 PR 为 github.com/rust-lang/rust/pull/22526 。跟踪 malloc 优化的问题是 github.com/rust-lang/rust/issues/22159
  • @ker 谢谢!看起来它只处理对 malloc 的未使用调用。在这种情况下,分配的空间肯定想要被使用(并且被重用!),所以我怀疑这些会发挥作用。
【解决方案2】:

截至 2021 年,Rust 能够优化堆分配和内联 vtable 方法调用 (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();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-09
    • 2012-08-19
    • 2020-10-03
    • 2021-05-18
    • 1970-01-01
    • 2016-10-25
    • 1970-01-01
    相关资源
    最近更新 更多