rust中的别名(aliasing)

rust May 6, 2021

前言

最近看rust死灵书,讲到了在rust programming languages那本书中没有提到的一些知识,记录下。

什么是别名(aliasing)

提到aliasing就要说到为什么会有这个概念——在死灵书的关于reference(引用)讲到了两个规则:

  1. 一个引用不能存活超过他的引用对象(A reference cannot outlive its referent)
  2. 一个可变引用不能被别名(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

别名分析的好处

有个这个的好处是什么呢?对于开发者来说感知不大,但是对于编译器来说有很大的益处。

  1. 将值放到高速缓存中,因为在一段作用域里面,没有人去修改内存,所以不需要同步。
  2. 省略了读操作,得益于上面的好处。
  3. 省略了一些写回操作,因为在下一次写内存前,内存不会有读取。(我理解是可以直接写到缓存里面,因为在当前的作用域里面,外部的访问,不需要做内存同步,只有在当前作用域结束,才需要写回到内存)
  4. 移动或重排没有依赖的读写顺序

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

总结

因为以前没有做过这么底层的东西,从来没有考虑过类似的事情,这次是涨姿势了。