| # Aliasing |
| |
| First off, let's get some important caveats out of the way: |
| |
| * We will be using the broadest possible definition of aliasing for the sake |
| of discussion. Rust's definition will probably be more restricted to factor |
| in mutations and liveness. |
| |
| * We will be assuming a single-threaded, interrupt-free, execution. We will also |
| be ignoring things like memory-mapped hardware. Rust assumes these things |
| don't happen unless you tell it otherwise. For more details, see the |
| [Concurrency Chapter](concurrency.html). |
| |
| With that said, here's our working definition: variables and pointers *alias* |
| if they refer to overlapping regions of memory. |
| |
| ## Why Aliasing Matters |
| |
| So why should we care about aliasing? |
| |
| Consider this simple function: |
| |
| ```rust |
| fn compute(input: &u32, output: &mut u32) { |
| if *input > 10 { |
| *output = 1; |
| } |
| if *input > 5 { |
| *output *= 2; |
| } |
| // remember that `output` will be `2` if `input > 10` |
| } |
| ``` |
| |
| We would *like* to be able to optimize it to the following function: |
| |
| ```rust |
| fn compute(input: &u32, output: &mut u32) { |
| let cached_input = *input; // keep `*input` in a register |
| if cached_input > 10 { |
| // If the input is greater than 10, the previous code would set the output to 1 and then double it, |
| // resulting in an output of 2 (because `>10` implies `>5`). |
| // Here, we avoid the double assignment and just set it directly to 2. |
| *output = 2; |
| } else if cached_input > 5 { |
| *output *= 2; |
| } |
| } |
| ``` |
| |
| In Rust, this optimization should be sound. For almost any other language, it |
| wouldn't be (barring global analysis). This is because the optimization relies |
| on knowing that aliasing doesn't occur, which most languages are fairly liberal |
| with. Specifically, we need to worry about function arguments that make `input` |
| and `output` overlap, such as `compute(&x, &mut x)`. |
| |
| With that input, we could get this execution: |
| |
| <!-- ignore: expanded code --> |
| ```rust,ignore |
| // input == output == 0xabad1dea |
| // *input == *output == 20 |
| if *input > 10 { // true (*input == 20) |
| *output = 1; // also overwrites *input, because they are the same |
| } |
| if *input > 5 { // false (*input == 1) |
| *output *= 2; |
| } |
| // *input == *output == 1 |
| ``` |
| |
| Our optimized function would produce `*output == 2` for this input, so the |
| correctness of our optimization relies on this input being impossible. |
| |
| In Rust we know this input should be impossible because `&mut` isn't allowed to be |
| aliased. So we can safely reject its possibility and perform this optimization. |
| In most other languages, this input would be entirely possible, and must be considered. |
| |
| This is why alias analysis is important: it lets the compiler perform useful |
| optimizations! Some examples: |
| |
| * keeping values in registers by proving no pointers access the value's memory |
| * eliminating reads by proving some memory hasn't been written to since last we read it |
| * eliminating writes by proving some memory is never read before the next write to it |
| * moving or reordering reads and writes by proving they don't depend on each other |
| |
| These optimizations also tend to prove the soundness of bigger optimizations |
| such as loop vectorization, constant propagation, and dead code elimination. |
| |
| In the previous example, we used the fact that `&mut u32` can't be aliased to prove |
| that writes to `*output` can't possibly affect `*input`. This lets us cache `*input` |
| in a register, eliminating a read. |
| |
| By caching this read, we knew that the write in the `> 10` branch couldn't |
| affect whether we take the `> 5` branch, allowing us to also eliminate a |
| read-modify-write (doubling `*output`) when `*input > 10`. |
| |
| The key thing to remember about alias analysis is that writes are the primary |
| hazard for optimizations. That is, the only thing that prevents us |
| from moving a read to any other part of the program is the possibility of us |
| re-ordering it with a write to the same location. |
| |
| For instance, we have no concern for aliasing in the following modified version |
| of our function, because we've moved the only write to `*output` to the very |
| end of our function. This allows us to freely reorder the reads of `*input` that |
| occur before it: |
| |
| ```rust |
| fn compute(input: &u32, output: &mut u32) { |
| let mut temp = *output; |
| if *input > 10 { |
| temp = 1; |
| } |
| if *input > 5 { |
| temp *= 2; |
| } |
| *output = temp; |
| } |
| ``` |
| |
| We're still relying on alias analysis to assume that `input` doesn't alias |
| `temp`, but the proof is much simpler: the value of a local variable can't be |
| aliased by things that existed before it was declared. This is an assumption |
| every language freely makes, and so this version of the function could be |
| optimized the way we want in any language. |
| |
| This is why the definition of "alias" that Rust will use likely involves some |
| notion of liveness and mutation: we don't actually care if aliasing occurs if |
| there aren't any actual writes to memory happening. |
| |
| Of course, a full aliasing model for Rust must also take into consideration things like |
| function calls (which may mutate things we don't see), raw pointers (which have |
| no aliasing requirements on their own), and UnsafeCell (which lets the referent |
| of an `&` be mutated). |