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;
}

通过这样改写,可以让 outputinput 之间互不依赖,这样编译器就可以对指令进行重排序(可以对 input 的读取进行重排序),从而利用 SIMD 加速程序的运行。

通过上面的例子,我们可以发现,别名分析中写操作是影响优化的关键。比如在上面的优化中,因为 input 没有被写入过,所以才能将其存放到寄存器中,也正是因为 input 没有写入过,我们才能消除一次对其的读取(对应于第一个优化程序)。对于第二种优化,编译器虽然可以对其做指令重排序,但是 *output = temp 必须得是最后一个执行的操作

参考资料: Rustonomicon