引言:Rust编译器的谜题
想象一下:你在用Rust编程,你是一个真正的内存安全爱好者。你知道两个指向同一数据的可变引用(&mut T)永远不能同时存在。这是借用规则(borrowing rules)的主要原则之一。Rust编译器严格遵循这一点。
看起来,如果编译器确信两个&mut不重叠(不是别名),它应该利用这一知识进行激进优化。例如,重排操作、将值缓存到寄存器中、避免重新读取内存。但矛盾的是:Rust编译器(LLVM)通常不这样做。为什么?这看起来如此合理!
这个问题在Stack Overflow上获得了数千次浏览。今天,我们将像真正的侦探一样,解开这个谜题。我们不仅仅是阅读答案——我们将理解深层原因,这些原因与C语言遗产、LLVM以及优化的工作原理有关。开始吧!
1. Rust的黄金法则:两个&mut始终指向不同的数据
回顾一下基础知识。Rust中有两种引用:
- 不可变引用(
&T):可以有任意多个,但不能修改数据。 - 可变引用(
&mut T):只能有一个,但可以修改数据。
这个规则对于&mut来说就是noalias。理论上,编译器可以假设:“如果我看到两个&mut i32,它们一定指向内存中不同的位置。”
但让我们看一个简单的例子,它展示了问题所在:
fn example(x: &mut i32, y: &mut i32) -> i32 { *x = 42; *y = 10; *x // 期望返回42,但是...}这里很明显:x和y是不同的变量。Rust保证它们不重叠。编译器本可以直接返回42,而不执行赋值操作。但事实果真如此吗?
2. 问题:LLVM对Rust保证的“视而不见”
我们故事的主角是——LLVM(低级虚拟机)。Rust使用LLVM作为后端来生成机器码。LLVM是一个强大的优化器,但它不了解Rust的规则。它只知道C/C++的规则。
在C/C++中,有关键字restrict(C99中),它告诉编译器:“这个引用是唯一的,没有其他指针指向这些数据。”但在C/C++中,restrict是程序员的承诺,而不是编译器的保证。如果程序员违反承诺——行为未定义。
而Rust提供了铁一般的保证。但为了将这个保证传递给LLVM,Rust前端必须在LLVM IR(中间表示)中明确地将&mut标记为noalias属性。
有趣的部分来了。Rust只在特定情况下将&mut标记为noalias。而在某些情况下则不标记。为什么?
2.1. 历史遗留问题:UnsafeCell和Unsafe代码
Rust内部有一个类型UnsafeCell<T>。它是Cell、RefCell、Mutex等允许内部可变性(interior mutability)的类型的构建块。
如果你有一个&mut UnsafeCell<i32>,你可以通过它获取原始指针,甚至通过不可变引用修改数据。这打破了noalias保证!
为了避免未定义行为,Rust编译器不会在&mut上设置noalias,如果其后可能隐藏着UnsafeCell。但问题更深层。
3. 实际例子:为什么优化不起作用
回到函数example