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

例如,temporary_assignmentsinlined慢还是不如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)
}

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

推荐答案

TLDR:

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

Slightly longer TLDR:

虽然编写(语义等价的)函数的不同方法可能会导致略有不同的CPU指令和性能,但通常在中间变量的数量和由现代优化编译器(如rustc)生成的程序集质量之间没有相关性(既不是正相关,也不是负相关性).在许多情况下,输出不会有任何不同.

如果你看这optimized assembly output个 对于你的两个例子,你会发现它几乎是相同的, 应该也差不多.

背景:

就像C/C++一样,Rust是一种静态编译语言,有一个优化编译器后端(在Rustc的情况下是LLVM).

你的CPU没有‘变量’的概念,它有寄存器和内存. 因此,编译器分析您编写的源代码,并try 找出使用您的CPU指令集来表达该语义的最有效方式.

带有LLVM后端的Rust编译器通过以下方式做到这一点

  1. 将源代码转换为更接近实际机器指令的中间表示(IR):LLVM-IR
  2. 使用许多算法和启发式(称为optimization passes)来转换此IR,试图使其更有效率.
  3. 将优化的LLVM-IR转换为用于您的实际目标体系 struct 的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
}

如您所见,此表示法使用virtual registers(例如%7)表示字面意义上的every single operation.这些virtual registers稍后将在register allocation期间转换为您的CPU体系 struct 的实际物理寄存器.

所以你的源代码变量已经go 掉了before serious opimization work has even begun.

现在,这个IR将运行许多优化过程.一些有趣的例子包括:

  • mem2reg:不是将数据存储在内存中并使用loadstore指令检索它,而是只需将数据保存在寄存器中并直接对其进行操作(可以显著提高效率,但并不总是可能的,因为我们可能传递了依赖于具有内存地址的数据的引用). 如果您在示例中复制任何struct,则此优化过程(与其他优化过程相结合)将有助于消除许多不必要的副本.
  • cse:公共子表达式消除:重用我们已经在其他地方计算的结果,而不是重新计算它们.
  • inlining:不调用函数,而是将该函数的IR复制粘贴到我们的函数中.这为优化器提供了更多关于正在发生的事情的上下文(majorly帮助其他优化通过),但当然也会增加二进制大小,因为我们复制了代码,所以我们不能总是这样做.一个好的内联策略通常被认为是优化编译器最关键的部分.这也是为什么虚函数调用(或Rust中的dyn特征调用)通常被认为是昂贵的,因为编译器通常不能内联它们.
  • 更多,比如循环展开,标量提升,...

虽然像LLVM这样的后端需要做大量的工作才能产生良好的组装,但这些工具并不神奇.如果你做了编译器不能"理解"的事情(==有一个优化通行证),它不会对你有帮助.因此,如果性能很关键,那么可以使用像Fantect Goodbolt.org这样的工具来弄清楚具体 case 中发生了什么.

但最重要的是,在不了解实际情况的情况下,请不要过早地进行奇怪的"优化",比如为了"性能"而省略会提高可读性的中间变量.

注意事项:

  • 上面假设我们讨论的是移动整数、浮点数或引用等小而简单的变量.如果你 Select 一个像Vec这样的复杂 struct ,或者是一个大的数组,那么这当然是不同的.(但在某些情况下,编译器甚至可以将其优化.)
  • 许多优化过程在Debug模式下是禁用的,因此请确保对Release版本进行基准测试.

Rust相关问答推荐

为什么类型需要在这个代码中手动指定,在rust?

为什么BitVec缺少Serialize trait?

在执行其他工作的同时,从共享裁判后面的VEC中删除重复项

在Rust中显式装箱受生存期限制的转换闭包

将数组转换为HashMap的更简单方法

无法从流中读取Redis请求

在Rust中声明和定义一个 struct 体有什么区别

如何使用reqwest进行异步请求?

在 Rust 中,在需要引用 self 的 struct 体方法中使用闭包作为 while 循环条件

如何迭代存储在 struct 中的字符串向量而不移动它们?

为什么 js_sys Promise::new 需要 FnMut?

如何在 Rust 中打印 let-else 语句中的错误?

tokio::spawn 有和没有异步块

需要一个有序向量来进行 struct 初始化

如何为已实现其他相关 std trait 的每个类型实现一个 std Trait

Rust与_有何区别?

为什么数组不像向量那样在 for 块之后移动?

如何在 Rust 中编写修改 struct 的函数

为什么可以从闭包中返回私有 struct

编写 TOML 文件以反序列化为 struct 中的枚举