与仅将变量的文字包含在表达式中相比,为变量分配中间值是否会产生运行时成本?
例如
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)
}
与使用变量存储中间值相比,在什么情况下以这种方式“内联”表达式可以提供运行时成本改进?
不,中间变量在 Rust 中没有运行时成本。
虽然编写(语义上等效)函数的不同方式可能会导致 CPU 指令和性能略有不同,但中间变量的数量和现代优化生成的汇编质量之间通常不存在相关性(无论是正相关还是负相关)像 rustc 这样的编译器。在许多情况下,输出根本不会有不同。
如果您查看优化的装配输出 对于你的两个例子,你会发现它几乎是相同的, 并且也应该表现得差不多。
就像 C/C++ 一样,Rust 是一种静态编译语言,具有优化编译器后端(对于 rustc 来说是 LLVM)。
你的CPU没有“变量”的概念,它有寄存器和内存。 因此,编译器会分析您编写的源代码,并尝试找出使用 CPU 指令集表达该语义的最有效方法。
带有 LLVM 后端的 Rust 编译器通过以下方式实现这一点:
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 特征调用)通常被认为是昂贵的,因为编译器通常无法内联这些。 还有更多,如循环展开、标量提升……