TLDR:
不,中间变量在Rust中没有运行时成本.
Slightly longer TLDR:
虽然编写(语义等价的)函数的不同方法可能会导致略有不同的CPU指令和性能,但通常在中间变量的数量和由现代优化编译器(如rustc)生成的程序集质量之间没有相关性(既不是正相关,也不是负相关性).在许多情况下,输出不会有任何不同.
如果你看这optimized assembly output个
对于你的两个例子,你会发现它几乎是相同的,
应该也差不多.
背景:
就像C/C++一样,Rust是一种静态编译语言,有一个优化编译器后端(在Rustc的情况下是LLVM).
你的CPU没有‘变量’的概念,它有寄存器和内存.
因此,编译器分析您编写的源代码,并try 找出使用您的CPU指令集来表达该语义的最有效方式.
带有LLVM后端的Rust编译器通过以下方式做到这一点
- 将源代码转换为更接近实际机器指令的中间表示(IR):LLVM-IR
- 使用许多算法和启发式(称为optimization passes)来转换此IR,试图使其更有效率.
- 将优化的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
:不是将数据存储在内存中并使用load
和store
指令检索它,而是只需将数据保存在寄存器中并直接对其进行操作(可以显著提高效率,但并不总是可能的,因为我们可能传递了依赖于具有内存地址的数据的引用).
如果您在示例中复制任何struct
,则此优化过程(与其他优化过程相结合)将有助于消除许多不必要的副本.
cse
:公共子表达式消除:重用我们已经在其他地方计算的结果,而不是重新计算它们.
inlining
:不调用函数,而是将该函数的IR复制粘贴到我们的函数中.这为优化器提供了更多关于正在发生的事情的上下文(majorly帮助其他优化通过),但当然也会增加二进制大小,因为我们复制了代码,所以我们不能总是这样做.一个好的内联策略通常被认为是优化编译器最关键的部分.这也是为什么虚函数调用(或Rust中的dyn特征调用)通常被认为是昂贵的,因为编译器通常不能内联它们.
- 更多,比如循环展开,标量提升,...
虽然像LLVM这样的后端需要做大量的工作才能产生良好的组装,但这些工具并不神奇.如果你做了编译器不能"理解"的事情(==有一个优化通行证),它不会对你有帮助.因此,如果性能很关键,那么可以使用像Fantect Goodbolt.org这样的工具来弄清楚具体 case 中发生了什么.
但最重要的是,在不了解实际情况的情况下,请不要过早地进行奇怪的"优化",比如为了"性能"而省略会提高可读性的中间变量.
注意事项:
- 上面假设我们讨论的是移动整数、浮点数或引用等小而简单的变量.如果你 Select 一个像
Vec
这样的复杂 struct ,或者是一个大的数组,那么这当然是不同的.(但在某些情况下,编译器甚至可以将其优化.)
- 许多优化过程在
Debug
模式下是禁用的,因此请确保对Release
版本进行基准测试.