diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0b34952..db72286 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -11,6 +11,7 @@ * [Other reprs](other-reprs.md) * [Ownership](ownership.md) * [References](references.md) + * [Aliasing](aliasing.md) * [Lifetimes](lifetimes.md) * [Limits of Lifetimes](lifetime-mismatch.md) * [Lifetime Elision](lifetime-elision.md) diff --git a/src/aliasing.md b/src/aliasing.md new file mode 100644 index 0000000..dc846f1 --- /dev/null +++ b/src/aliasing.md @@ -0,0 +1,135 @@ +# Aliasing + +First off, let's get some important caveats out of this 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; + } +} +``` + +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 { + *output = 2; // x > 10 implies x > 5, so double and exit immediately + } 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: + +```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 let us cache `*input` +in a register, eliminating a read. + +By caching this read, we knew that the 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 `temp` doesn't alias +`input`, 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). + + + diff --git a/src/references.md b/src/references.md index 5d80f1e..294fe1c 100644 --- a/src/references.md +++ b/src/references.md @@ -1,12 +1,5 @@ # References -This section gives a high-level view of the memory model that *all* Rust -programs must satisfy to be correct. Safe code is statically verified -to obey this model by the borrow checker. Unsafe code may go above -and beyond the borrow checker while still satisfying this model. The borrow -checker may also be extended to allow more programs to compile, as long as -this more fundamental model is satisfied. - There are two kinds of reference: * Shared reference: `&` @@ -17,161 +10,22 @@ Which obey the following rules: * A reference cannot outlive its referent * A mutable reference cannot be aliased -That's it. That's the whole model. Of course, we should probably define -what *aliased* means. To define aliasing, we must define the notion of -*paths* and *liveness*. - - -**NOTE: The model that follows is generally agreed to be dubious and have -issues. It's ok-ish as an intuitive model, but fails to capture the desired -semantics. We leave this here to be able to use notions introduced here in later -sections. This will be significantly changed in the future. TODO: do that.** - - -# Paths - -If all Rust had were values (no pointers), then every value would be uniquely -owned by a variable or composite structure. From this we naturally derive a -*tree* of ownership. The stack itself is the root of the tree, with every -variable as its direct children. Each variable's direct children would be their -fields (if any), and so on. - -From this view, every value in Rust has a unique *path* in the tree of -ownership. Of particular interest are *ancestors* and *descendants*: if `x` owns -`y`, then `x` is an ancestor of `y`, and `y` is a descendant of `x`. Note -that this is an inclusive relationship: `x` is a descendant and ancestor of -itself. - -We can then define references as simply *names* for paths. When you create a -reference, you're declaring that an ownership path exists to this address -of memory. - -Tragically, plenty of data doesn't reside on the stack, and we must also -accommodate this. Globals and thread-locals are simple enough to model as -residing at the bottom of the stack (though we must be careful with mutable -globals). Data on the heap poses a different problem. - -If all Rust had on the heap was data uniquely owned by a pointer on the stack, -then we could just treat such a pointer as a struct that owns the value on the -heap. Box, Vec, String, and HashMap, are examples of types which uniquely -own data on the heap. - -Unfortunately, data on the heap is not *always* uniquely owned. Rc for instance -introduces a notion of *shared* ownership. Shared ownership of a value means -there is no unique path to it. A value with no unique path limits what we can do -with it. - -In general, only shared references can be created to non-unique paths. However -mechanisms which ensure mutual exclusion may establish One True Owner -temporarily, establishing a unique path to that value (and therefore all -its children). If this is done, the value may be mutated. In particular, a -mutable reference can be taken. - -The most common way to establish such a path is through *interior mutability*, -in contrast to the *inherited mutability* that everything in Rust normally uses. -Cell, RefCell, Mutex, and RWLock are all examples of interior mutability types. -These types provide exclusive access through runtime restrictions. - -An interesting case of this effect is Rc itself: if an Rc has refcount 1, -then it is safe to mutate or even move its internals. Note however that the -refcount itself uses interior mutability. - -In order to correctly communicate to the type system that a variable or field of -a struct can have interior mutability, it must be wrapped in an UnsafeCell. This -does not in itself make it safe to perform interior mutability operations on -that value. You still must yourself ensure that mutual exclusion is upheld. - - - +That's it. That's the whole model references follow. -# Liveness +Of course, we should probably define what *aliased* means. -Note: Liveness is not the same thing as a *lifetime*, which will be explained -in detail in the next section of this chapter. +```text +error[E0425]: cannot find value `aliased` in this scope + --> :2:20 + | +2 | println!("{}", aliased); + | ^^^^^^^ not found in this scope -Roughly, a reference is *live* at some point in a program if it can be -dereferenced. Shared references are always live unless they are literally -unreachable (for instance, they reside in freed or leaked memory). Mutable -references can be reachable but *not* live through the process of *reborrowing*. - -A mutable reference can be reborrowed to either a shared or mutable reference to -one of its descendants. A reborrowed reference will only be live again once all -reborrows derived from it expire. For instance, a mutable reference can be -reborrowed to point to a field of its referent: - -```rust -let x = &mut (1, 2); -{ - // reborrow x to a subfield - let y = &mut x.0; - // y is now live, but x isn't - *y = 3; -} -// y goes out of scope, so x is live again -*x = (5, 7); +error: aborting due to previous error ``` -It is also possible to reborrow into *multiple* mutable references, as long as -they are *disjoint*: no reference is an ancestor of another. Rust -explicitly enables this to be done with disjoint struct fields, because -disjointness can be statically proven: - -```rust -let x = &mut (1, 2); -{ - // reborrow x to two disjoint subfields - let y = &mut x.0; - let z = &mut x.1; - - // y and z are now live, but x isn't - *y = 3; - *z = 4; -} -// y and z go out of scope, so x is live again -*x = (5, 7); -``` - -However it's often the case that Rust isn't sufficiently smart to prove that -multiple borrows are disjoint. *This does not mean it is fundamentally illegal -to make such a borrow*, just that Rust isn't as smart as you want. - -To simplify things, we can model variables as a fake type of reference: *owned* -references. Owned references have much the same semantics as mutable references: -they can be re-borrowed in a mutable or shared manner, which makes them no -longer live. Live owned references have the unique property that they can be -moved out of (though mutable references *can* be swapped out of). This power is -only given to *live* owned references because moving its referent would of -course invalidate all outstanding references prematurely. - -As a local lint against inappropriate mutation, only variables that are marked -as `mut` can be borrowed mutably. - -It is interesting to note that Box behaves exactly like an owned reference. It -can be moved out of, and Rust understands it sufficiently to reason about its -paths like a normal variable. - - - - -# Aliasing - -With liveness and paths defined, we can now properly define *aliasing*: - -**A mutable reference is aliased if there exists another live reference to one -of its ancestors or descendants.** - -(If you prefer, you may also say the two live references alias *each other*. -This has no semantic consequences, but is probably a more useful notion when -verifying the soundness of a construct.) - -That's it. Super simple right? Except for the fact that it took us two pages to -define all of the terms in that definition. You know: Super. Simple. - -Actually it's a bit more complicated than that. In addition to references, Rust -has *raw pointers*: `*const T` and `*mut T`. Raw pointers have no inherent -ownership or aliasing semantics. As a result, Rust makes absolutely no effort to -track that they are used correctly, and they are wildly unsafe. +Unfortunately, Rust hasn't actually defined its aliasing model. 🙀 -**It is an open question to what degree raw pointers have alias semantics. -However it is important for these definitions to be sound that the existence of -a raw pointer does not imply some kind of live path.** +While we wait for the Rust devs to specify the semantics of their language, +let's use the next section to discuss what aliasing is in general, and why it +matters.