原文地址:https://doc.rust-lang.org/nomicon/aliasing.html

别名

首先,有几点重要声明:

  • 以下的讨论将采用最广泛意义上的别名的定义。而Rust的定义可能会更加严格,需要考虑到可变性和生命周期。
  • 我们假设程序都是单线程且不会中断的,同时也不会去考虑存储器映射之类的问题。除非特别指定,否则Rust默认这些事情不存在。更多的细节请见并发章节

基于这些,我们给出定义:当变量和指针表示的内存区域有重叠时,它们互为对方的别名。

为什么别名很重要

为什么我们要关注别名?

看下面这个简单的函数。

fn compute(input: &u32, output: &mut u32) {
    if *input > 10 {
        *output = 1;
    }
    if *input > 5 {
        *output *= 2;
    }
}

我们可能会这样优化它:

fn compute(input: &u32, output: &mut u32) {
    let cached_input = *input; // 将*input放入缓存
    if cached_input > 10 {
        *output = 2; // x > 5 则必然 x > 5,所以直接加倍并立即退出
    } else if cached_input > 5 {
        *output *= 2;
    }
}

在Rust中,这种优化是正确的。但对于其他几乎所有的语言,都是有错误的(除非编译器进行全局分析)。这是因为优化方案成立的前提是不存在别名,而绝大多数语言并不会限制这一点。例子中我们需要特别担心的是传递给inputoutput的参数可能会重合,比如comput(&x, &mut x)

对于上面的参数,程序流程会是这样:

                  //  input ==  output == 0xabad1dea
                  // *input == *output == 20
if *input > 10 {  // true (*input == 20)
    *output = 1;  // 同时覆盖了 *input,以为他们是一样的
} 
*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可能采用的“别名”定义与生命周期和可变性有关的原因:在没有写内存操作存在的情况下,我们实际上不需要关注是否存在别名。

当然,一个完整的别名模型也要考虑到诸如函数调用(可能改变我们不可见的内容)、裸指针(不存在限制别名的需求),以及UnsafeCell(允许被&引用的内容可变)。