别名
首先,让我们把一些重要的注意事项摆在前面
-
为了讨论,我们将使用最广泛的别名定义。Rust 的定义可能会更受限制,以考虑突变和活跃性。
-
我们将假设是单线程、无中断的执行。我们还将忽略诸如内存映射硬件之类的东西。除非你另行告知,否则 Rust 假设这些事情不会发生。有关更多详细信息,请参阅并发章节。
综上所述,这是我们的工作定义:如果变量和指针引用重叠的内存区域,则它们别名。
别名为什么重要
那么,我们为什么要关心别名呢?
考虑这个简单的函数
#![allow(unused)] fn main() { fn compute(input: &u32, output: &mut u32) { if *input > 10 { *output = 1; } if *input > 5 { *output *= 2; } // remember that `output` will be `2` if `input > 10` } }
我们希望能够将其优化为以下函数
#![allow(unused)] fn main() { fn compute(input: &u32, output: &mut u32) { let cached_input = *input; // keep `*input` in a register if cached_input > 10 { // If the input is greater than 10, the previous code would set the output to 1 and then double it, // resulting in an output of 2 (because `>10` implies `>5`). // Here, we avoid the double assignment and just set it directly to 2. *output = 2; } else if cached_input > 5 { *output *= 2; } } }
在 Rust 中,这种优化应该是安全的。对于几乎任何其他语言,它都不会安全(除非进行全局分析)。这是因为优化依赖于知道不会发生别名,而大多数语言在这方面都相当宽松。具体来说,我们需要担心使 `input` 和 `output` 重叠的函数参数,例如 `compute(&x, &mut x)`。
对于该输入,我们可能会得到这样的执行
// input == output == 0xabad1dea
// *input == *output == 20
if *input > 10 { // true (*input == 20)
*output = 1; // also overwrites *input, because they are the same
}
if *input > 5 { // false (*input == 1)
*output *= 2;
}
// *input == *output == 1
我们的优化函数会为此输入产生 `*output == 2`,因此我们优化的正确性取决于此输入是不可能的。
在 Rust 中,我们知道这种输入是不可能的,因为不允许对 `&mut` 进行别名。因此,我们可以安全地拒绝其可能性并执行此优化。在大多数其他语言中,此输入是完全有可能的,必须加以考虑。
这就是为什么别名分析很重要:它可以让编译器执行有用的优化!一些例子
- 通过证明没有指针访问该值的内存,将值保留在寄存器中
- 通过证明自上次读取以来某些内存尚未写入,消除读取
- 通过证明某些内存永远不会在下次写入之前被读取,消除写入
- 通过证明读取和写入彼此不依赖,移动或重新排序读取和写入
这些优化也往往证明了诸如循环向量化、常量传播和死代码消除等更大优化的合理性。
在前面的示例中,我们利用了 `&mut u32` 不能被别名这一事实来证明对 `*output` 的写入不可能影响 `*input`。这使我们可以将 `*input` 缓存在寄存器中,从而消除读取。
通过缓存此读取,我们知道当 `*input > 10` 时,`> 10` 分支中的写入不会影响我们是否采用 `> 5` 分支,从而允许我们消除读-修改-写(加倍 `*output`)。
关于别名分析,要记住的关键是,写入是优化的主要危害。也就是说,阻止我们将读取移动到程序任何其他部分的唯一原因是我们可能将其与写入同一位置的写入操作重新排序。
例如,在下面修改后的函数版本中,我们不担心别名,因为我们将对 `*output` 的唯一写入移动到了函数的末尾。这使我们可以自由地重新排序在其之前发生的 `*input` 的读取
#![allow(unused)] fn main() { fn compute(input: &u32, output: &mut u32) { let mut temp = *output; if *input > 10 { temp = 1; } if *input > 5 { temp *= 2; } *output = temp; } }
我们仍然依赖于别名分析来假设 `input` 不会别名 `temp`,但是证明要简单得多:局部变量的值不能被声明之前存在的事物别名。这是每种语言都自由假设的,因此该版本的函数可以在任何语言中以我们想要的方式进行优化。
这就是为什么 Rust 将使用的“别名”定义可能涉及一些关于活跃性和突变的概念:如果没有发生任何实际的内存写入,我们实际上并不关心是否发生别名。
当然,Rust 的完整别名模型还必须考虑诸如函数调用(可能会改变我们看不到的东西)、原始指针(它们本身没有别名要求)和 UnsafeCell(它允许 `&` 的引用被改变)之类的事情。