别名(Aliasing)
首先,让我们以这种方式得到一些重要的警告:
为了便于讨论,我们将使用最广泛的别名定义.Rust的定义可能更局限于变化(mutations)和活性(liveness)因素.
我们将假设一个单线程的,无中断的执行.我们也会忽略内存映射硬件之类的东西.Rust假设这些事情不会发生,除非你另有说明.有关更多详细信息,请参阅”并发(Concurrency)”一章.
话虽如此,这是我们的工作定义:变量和指针 别名(alias) ,如果它们引用重叠的内存区域.
为什么别名很重要(Why Aliasing Matters)
那我们为什么要关心别名呢?
考虑这个简单的函数:
fn compute(input: &u32, output: &mut u32) {
if *input > 10 {
*output = 1;
}
if *input > 5 {
*output *= 2;
}
}
我们 希望(like) 能够将其优化为以下函数:
fn compute(input: &u32, output: &mut u32) {
let cached_input = *input; // keep *input in a register
if cached_input > 10 {
*output = 2; // x > 10 implies x > 5, so double and exit immediately
} else if cached_input > 5 {
*output *= 2;
}
}
在Rust中,这种优化应该是合理的.对于几乎任何其他语言,它都不会(除非进行全局分析).这是因为优化依赖于知道不会出现别名,大多数语言都相当自由.具体来说,我们需要担心使输入和输出重叠的函数参数,例如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
缓存在寄存器中,从而消除读取.
通过缓存此读取,我们知道> 10
分支中的写入不会影响我们是否采用> 5
分支,允许我们在* input > 10
时也消除读取-修改-写入(两个*output
).
关于别名分析要记住的关键是写入是优化的主要危险.也就是说,阻止我们将读取移动到程序的任何其他部分的唯一因素是我们可以通过写入相同位置来重新排序它.
例如,我们不关心函数的以下修改版本中的别名,因为我们已经将唯一的写入*output
移动到函数的最后.这允许我们自由地重新排序在它之前发生的*input
的读取:
fn compute(input: &u32, output: &mut u32) {
let mut temp = *output;
if *input > 10 {
temp = 1;
}
if *input > 5 {
temp *= 2;
}
*output = temp;
}
我们仍然依赖于别名分析来假设temp
不会对input
进行别名,但证明要简单得多:局部变量的值不能被声明之前存在的东西别名化.这是每种语言自由发挥的假设,因此该版本的函数可以按照我们想要的任何语言中的方式进行优化.
这就是为什么Rust将使用的”别名(alias)”的定义可能涉及一些活性和变化的概念:如果没有任何实际写入内存发生,我们实际上并不关心别名是否出现混叠.
当然,Rust的完整别名模型还必须考虑诸如函数调用(可能会改变我们看不到的东西),原始指针(它们自身没有别名要求)和UnsafeCell(它允许&
的引用对象被改变)之类的事情.