rust中的别名(aliasing)
前言
最近看rust死灵书,讲到了在rust programming languages那本书中没有提到的一些知识,记录下。
什么是别名(aliasing)
提到aliasing就要说到为什么会有这个概念——在死灵书的关于reference(引用)讲到了两个规则:
- 一个引用不能存活超过他的引用对象(A reference cannot outlive its referent)
- 一个可变引用不能被别名(A mutable reference cannot be aliased)
在rust讨论别名,我们采用如下定义:当某个变量和某个指针表示的内存区域有重叠时,它们互为对方的别名。
ps:c++里面的引用类似于别名的存在,c++里面的引用的概念和rust的引用概念是不一样的。
为什么别名很重要?
我们看一个简单的rust函数:
fn compute(input: &u32, output: &mut u32) {
if *input > 10 {
*output = 1;
}
if *input > 5 {
*output *= 2;
}
}
fn main() {
let mut input:u32 = 22;
let mut output:u32 = 32;
compute(&input, &mut output);
println!("input {}", input);
println!("output {}", output);
}
我们可能会这样优化它:
fn compute(input: &u32, output: &mut u32) {
let cached_input = *input; // 将*input放入缓存
if cached_input > 10 {
*output = 2; // x > 10 则必然 x > 5,所以直接加倍并立即退出
} else if cached_input > 5 {
*output *= 2;
}
}
fn main() {
let mut input:u32 = 22;
let mut output:u32 = 32;
compute(&input, &mut input); // 这里会报错,因为不符合引用的第二条规则。按照之前的所有权的规则理解就是,在一个作用域里面只能有一个可变引用。
println!("input {}", input);
println!("output {}", output);
}
|
| compute(&input, &mut input);
| ------- ------ ^^^^^^^^^^ mutable borrow occurs here
| | |
| | immutable borrow occurs here
| immutable borrow later used by call
在Rust中,这种优化是正确的。但对于其他几乎所有的语言,都是有错误的(除非编译器进行全局分析)。这是因为优化方案成立的前提是不存在别名,而绝大多数语言并不会限制这一点。看一个c++中的例子:
void compute(const int* input, int* output) {
if (*input > 10) {
*output = 1;
}
if (*input > 5) {
*output *= 2;
}
}
int main()
{
int input = 20;
int output = 10;
compute(&input, &input); // c++ 不会验证传入参数的别名重复
cout << "output " << output << endl;
cout << "input " << input << endl;
return 0;
}
上面的执行过程如下:
// 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
别名分析的好处
有个这个的好处是什么呢?对于开发者来说感知不大,但是对于编译器来说有很大的益处。
- 将值放到高速缓存中,因为在一段作用域里面,没有人去修改内存,所以不需要同步。
- 省略了读操作,得益于上面的好处。
- 省略了一些写回操作,因为在下一次写内存前,内存不会有读取。(我理解是可以直接写到缓存里面,因为在当前的作用域里面,外部的访问,不需要做内存同步,只有在当前作用域结束,才需要写回到内存)
- 移动或重排没有依赖的读写顺序
这些优化也可以进一步证明更大程度的优化的可行性,比如循环向量化、常量替换和不可达代码消除等。
总结
因为以前没有做过这么底层的东西,从来没有考虑过类似的事情,这次是涨姿势了。