别名
首先,有几点重要声明:
- 我们假设程序都是单线程且不会中断的,同时也不会去考虑存储器映射之类的问题。除非特别指定,否则Rust默认这些事情不存在。更多的细节请见并发章节。
基于这些,我们给出定义:当变量和指针表示的内存区域有重叠时,它们互为对方的别名。
为什么我们要关注别名?
看下面这个简单的函数。
我们可能会这样优化它:
对于上面的参数,程序流程会是这样:
我们优化过的函数的结果是*output == 2
,所以对于这样的输入参数,优化函数是不正确的。
在Rust中我们知道不会出现上面那样的输入参数,因为&mut
不允许存在别名。所以我们可以安全的忽略这种可能性而使用优化方案。对于大多数其他语言,这种输入的可能性是存在的,必须特别的考虑到。
这就是别名分析的重要性:它允许编译器做出一些有用的优化。举几个例子:
- 省略一些读操作,因为可以确定在上一次读内存之后,内存没有发生变化
- 移动或重排读写操作的顺序,因为可以确定它们并不互相依赖
这些优化也可以进一步证明更大程度的优化的可行性,比如循环向量化、常量替换和不可达代码消除等。
通过缓存读操作的结果,我们知道在>10
的分支中的写操作不会影响执行>5
分支的判断条件,这样我们在*input > 10
的情况下省略了一次读-改-写操作(加倍)。
关于别名分析需要记住的一个关键点是,写操作是优化的主要障碍。我们不能随意移动读操作的唯一原因,就是可能存在向相同位置写数据的操作,这种移动会破坏他们之间的顺序关系。
比如,下面这个版本的函数中,我们不需要担心别名问题,因为我们把唯一的一次写*output
的操作放到了函数的最后。这让我们可以随意地改变之前的读*input
操作的顺序:
我们仍然需要别名分析来证明temp
不是input
的别名,但是这时的证明过程要简单得多:一个本地别量不可能是在它的声明之前就存在的变量的别名。这是所有编程语言共有的一个前提,所以这一版本的函数可以按照与其他语言相同的方式去优化它。
这也就是Rust可能采用的“别名”定义与生命周期和可变性有关的原因:在没有写内存操作存在的情况下,我们实际上不需要关注是否存在别名。