Introduction: The Rust Compiler Puzzle
Imagine: you're writing in Rust, you're a true memory safety enthusiast. You know that two mutable references (&mut T) to the same data can never exist simultaneously. This is one of the main borrowing rules. The Rust compiler strictly enforces this.
It would seem that if the compiler is certain that two &mut references do not overlap (are not aliases), it should use this knowledge for aggressive optimization. For example, reordering operations, caching values in registers, not re-reading memory. But here's the paradox: the Rust compiler (LLVM) often doesn't do this. Why? It seems so logical!
This question has gathered thousands of views on Stack Overflow. And today, like real detectives, we'll get to the bottom of this mystery. We won't just read the answer — we'll understand the deep reasons, related to C heritage, LLVM, and how optimization works. Let's go!
1. Rust's Golden Rule: Two &mut — Always Different Data
Let's review the basics. Rust has two types of references:
- Immutable (
&T): there can be any number of them, but the data cannot be changed. - Mutable (
&mut T): there can be only one, but it can be changed.
This rule is noalias for &mut. The compiler, in theory, can assume: "If I see two &mut i32, they definitely point to different memory locations."
But let's look at a simple example that demonstrates the problem:
fn example(x: &mut i32, y: &mut i32) -> i32 { *x = 42; *y = 10; *x // Expect 42, but...}Here everything is obvious: x and y are different variables. Rust guarantees they don't overlap. The compiler could simply return 42 without performing the assignments. But is that really the case?
2. The Problem: LLVM and Its "Blindness" to Rust's Guarantees
The main character of our story is LLVM (Low Level Virtual Machine). Rust uses LLVM as a backend for generating machine code. LLVM is a powerful optimizer, but it doesn't know about Rust's rules. It only knows about C/C++ rules.
In C/C++, there is the restrict keyword (in C99), which tells the compiler: "This pointer is unique, no one else points to this data." But in C/C++, restrict is a promise from the programmer, not a guarantee from the compiler. If the programmer breaks the promise — undefined behavior.
Rust, however, gives a rock-solid guarantee. But to pass this guarantee to LLVM, the Rust frontend must explicitly mark &mut with the noalias attribute in LLVM IR (intermediate representation).
And here's where it gets interesting. Rust marks &mut as noalias only in certain cases. In some cases — it doesn't. Why?
2.1. Historical Problem: UnsafeCell and Unsafe Code
Inside Rust, there is the UnsafeCell<T> type. This is the building block for Cell, RefCell, Mutex, and other types that allow interior mutability.
If you have &mut UnsafeCell<i32>, you can obtain a raw pointer through it and modify the data even through an immutable reference. This breaks the noalias guarantee!
To avoid undefined behavior, the Rust compiler does not set noalias on &mut if UnsafeCell might be hidden behind it. But the problem runs deeper.
3. Real Example: Why Optimization Doesn't Work
Let's return to the example function