Skip to content

Commit

Permalink
Merge pull request rust-lang#27 from Gankro/reref
Browse files Browse the repository at this point in the history
rewrite references.md
  • Loading branch information
steveklabnik authored Jun 12, 2017
2 parents c0e8c56 + c4822cd commit 794c2d6
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 159 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
135 changes: 135 additions & 0 deletions src/aliasing.md
Original file line number Diff line number Diff line change
@@ -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).



172 changes: 13 additions & 159 deletions src/references.md
Original file line number Diff line number Diff line change
@@ -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: `&`
Expand All @@ -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
--> <rust.rs>: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.

0 comments on commit 794c2d6

Please sign in to comment.