Rustonomicon Ownership
Ownership
Ownership 是 Rust 的显著特征,它允许 Rust 在不使用 GC 的情况下做到内存安全。为了做到这一点,Rust 将引用分为两类:Shared reference 和 Mutable reference,并制定了下面的两条规则:
- A reference cannot outlive its referent
- A mutable reference cannot be aliased
规则一规定了一个引用不能比其所引用的对象获得还长,这就确保了不会产生悬垂引用。规则二强制要求一个可变引用不能有别名,即可变引用和不可变引用不能同时存在,并且同时只能有一个可变引用。
Aliasing
上面我们提到了别名,那么何为别名呢?在 Rustonomicon 中这样定义:
Definition: variables and pointers alias if they refer to overlapping regions of memory.
也就是说,只要变量和指针引用的内存区域相重叠,就说他们互为别名。就像上面说的,一块内存区域的可变引用和不可变引用互为别名。
通过别名分析,可以让编译器更好地对代码进行优化,比如:
- 如果没有指针放访问某个 value 的内存,那么可以 value 存储在寄存器中
- 如果某个内存自从上一次读取之后没有被写过,可以消除对其的读取操作
- 如果某个内存自从上一次修改后没有被读取过,那么可以消除对其的写操作
- 如果读写操作不相互依赖,那么可以对他们重排序
比如下面这个程序:
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`
}
由于 input
没有被写入过,所以可以将 input
的结果缓存在寄存器中,这样就减少了一次内存的读取。同时,output
在写入 *output = 1
后没有被读取过,所以在 *input > 10
的情况下,可以消除第一次的写 *output = 1
。所以一个可能的优化如下:
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;
}
}
这种优化在 C/C++ 等语言中可能是行不通的,因为如果 input == output
,那么这种优化可能会导致得到错误的结果。比如 input == outpout
, input = 11
的情况下,正确结果应该是 1,但是优化后的代码得到的结果是 2。所以编译器是不能够进行这种优化的。
但是在 Rust 中这种优化是可能的,因为根据借用的规则,一个可变借用不能与不可变借用同时存在,所有我们可以断言:input != output
,因此编译器可以大胆地做这种优化。
这个例子可能有另一种形式的优化:
fn compute(input: &u32, output: &mut u32) {
let mut temp = *output;
if *input > 10 {
temp = 1;
}
if *input > 5 {
temp *= 2;
}
*output = temp;
}
通过这样改写,可以让 output
与 input
之间互不依赖,这样编译器就可以对指令进行重排序(可以对 input
的读取进行重排序),从而利用 SIMD 加速程序的运行。
通过上面的例子,我们可以发现,别名分析中写操作是影响优化的关键。比如在上面的优化中,因为 input
没有被写入过,所以才能将其存放到寄存器中,也正是因为 input
没有写入过,我们才能消除一次对其的读取(对应于第一个优化程序)。对于第二种优化,编译器虽然可以对其做指令重排序,但是 *output = temp
必须得是最后一个执行的操作
参考资料: Rustonomicon