别名

    首先,有几点重要声明:

    • 我们假设程序都是单线程且不会中断的,同时也不会去考虑存储器映射之类的问题。除非特别指定,否则Rust默认这些事情不存在。更多的细节请见并发章节

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

    为什么我们要关注别名?

    看下面这个简单的函数。

    我们可能会这样优化它:

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

    我们优化过的函数的结果是*output == 2,所以对于这样的输入参数,优化函数是不正确的。

    在Rust中我们知道不会出现上面那样的输入参数,因为&mut不允许存在别名。所以我们可以安全的忽略这种可能性而使用优化方案。对于大多数其他语言,这种输入的可能性是存在的,必须特别的考虑到。

    这就是别名分析的重要性:它允许编译器做出一些有用的优化。举几个例子:

    • 省略一些读操作,因为可以确定在上一次读内存之后,内存没有发生变化
    • 移动或重排读写操作的顺序,因为可以确定它们并不互相依赖

    这些优化也可以进一步证明更大程度的优化的可行性,比如循环向量化、常量替换和不可达代码消除等。

    通过缓存读操作的结果,我们知道在>10的分支中的写操作不会影响执行>5分支的判断条件,这样我们在*input > 10的情况下省略了一次读-改-写操作(加倍)。

    关于别名分析需要记住的一个关键点是,写操作是优化的主要障碍。我们不能随意移动读操作的唯一原因,就是可能存在向相同位置写数据的操作,这种移动会破坏他们之间的顺序关系。

    比如,下面这个版本的函数中,我们不需要担心别名问题,因为我们把唯一的一次写*output的操作放到了函数的最后。这让我们可以随意地改变之前的读*input操作的顺序:

    我们仍然需要别名分析来证明temp不是input的别名,但是这时的证明过程要简单得多:一个本地别量不可能是在它的声明之前就存在的变量的别名。这是所有编程语言共有的一个前提,所以这一版本的函数可以按照与其他语言相同的方式去优化它。

    这也就是Rust可能采用的“别名”定义与生命周期和可变性有关的原因:在没有写内存操作存在的情况下,我们实际上不需要关注是否存在别名。