Rust 中分配变量有运行时成本吗?

问题描述 投票:0回答:1

与仅将变量的文字包含在表达式中相比,为变量分配中间值是否会产生运行时成本?

例如

temporary_assignments
较慢或在其他方面不如
inlined

// Use variable assignment to assign names to the intermediate components of the 
// final answer, then combine those names in the final answer.
fn temporary_assignments(a: f32, b: f32, c: f32) -> f32 {
    let fourac = 4. * a * c;
    let discrim = b * b - fourac;
    let rad = discrim.sqrt();
    let denom = 2. * a;
    (-b + rad) / denom
}

// Express the entire final answer with literals.
fn inlined(a: f32, b: f32, c: f32) -> f32 {
    (-b + (b * b - 4. * a * c).sqrt()) / (2. * a)
}

与使用变量存储中间值相比,在什么情况下以这种方式“内联”表达式可以提供运行时成本改进?

performance rust optimization expression
1个回答
0
投票

总长:

不,中间变量在 Rust 中没有运行时成本。

稍微长一点的 TLDR:

虽然编写(语义上等效)函数的不同方式可能会导致 CPU 指令和性能略有不同,但中间变量的数量和现代优化生成的汇编质量之间通常不存在相关性(无论是正相关还是负相关)像 rustc 这样的编译器。在许多情况下,输出根本不会有不同。

如果您查看优化的装配输出 对于你的两个例子,你会发现它几乎是相同的, 并且也应该表现得差不多。

背景:

就像 C/C++ 一样,Rust 是一种静态编译语言,具有优化编译器后端(对于 rustc 来说是 LLVM)。

你的CPU没有“变量”的概念,它有寄存器和内存。 因此,编译器会分析您编写的源代码,并尝试找出使用 CPU 指令集表达该语义的最有效方法。

带有 LLVM 后端的 Rust 编译器通过以下方式实现这一点:

    将源代码转换为更接近实际机器指令的中间表示(IR):LLVM-IR
  1. 使用许多算法和启发式方法(称为
  2. 优化过程)来转换此 IR,试图使其更加高效。
  3. 将优化的 LLVM-IR 转换为适合您实际目标架构的 CPU 指令。 (然后再优化一些,这里不太相关)。
编译器优化可以做什么和不能做什么:

例如,这是适用于您的

inlined

 函数的(未优化的)LLVM-IR:

define float @inlined(float %0, float %1, float %2) unnamed_addr { %4 = fneg float %1 %5 = fmul float %1, %1 %6 = fmul float 4.000000e+00, %0 %7 = fmul float %6, %2 %8 = fsub float %5, %7 %9 = call float @"<sqrt_function>"(float %8) %10 = fadd float %4, %9 %11 = fmul float 2.000000e+00, %0 %12 = fdiv float %10, %11 ret float %12 }
如您所见,此表示使用 

虚拟寄存器 (例如 %7

)来表示字面上的 
每个操作的结果。这些虚拟寄存器稍后将在寄存器分配期间转变为CPU架构的实际物理寄存器。

所以你的源代码变量已经被消除了

在认真的优化工作开始之前

现在,这个 IR 将经历许多优化过程。一些有趣的是:

  • mem2reg
    :无需将数据存储在内存中并使用 
    load
    store
     指令检索数据,只需将数据保存在寄存器中并直接对其进行操作(可以显着提高效率,但并不总是可行,我们可能已经传递了依赖于具有内存地址的数据的引用)。
    如果您在示例中复制任何 
    struct
    ,此优化过程(与其他优化过程结合)将有助于消除许多不必要的副本。
  • cse
    :公共子表达式消除:在其他地方重用我们已经计算的结果,而不是重新计算它们。
  • inlining
    :不要调用函数,而是将该函数的 IR 复制粘贴到我们的函数中。这为优化器提供了更多关于正在发生的事情的上下文(这
    主要有助于其他优化过程),但当然可以在我们复制代码时增加二进制大小,所以我们不能总是这样做。良好的内联策略通常被认为是优化编译器最关键的部分。这也是为什么虚拟函数调用(或 Rust 中的 dyn 特征调用)通常被认为是昂贵的,因为编译器通常无法内联这些。 还有更多,如循环展开、标量提升……
  • 虽然需要做大量工作来让 LLVM 等后端发出良好的汇编,但这些工具并不神奇。如果你做了编译器不“理解”的事情(==有一个优化过程),它不会帮助你。因此,如果性能至关重要,请使用像神奇的 godbolt.org 这样的工具来弄清楚您的具体案例中发生了什么。
最重要的是,请不要在不了解实际情况的情况下进行奇怪的过早“优化”,例如省略为了“性能”而提高可读性的中间变量:)。

© www.soinside.com 2019 - 2024. All rights reserved.