Aliasing and mutability: which writes can affect a read?
Trace overlapping accesses, explain optimization assumptions, and distinguish interior mutability from unrestricted shared mutation.
On this page
The question for today
What you’ll understand
- Predict how overlapping storage changes the meaning of sequential operations.
- Explain why exclusive-access contracts support compiler transformations.
- Identify what interior-mutability wrappers and synchronization must still guarantee.
Two names can designate overlapping storage. That fact becomes important when one path writes and another path reads: a value you planned to reuse may have changed. Aliasing is therefore a correctness and optimization question even in a single thread. Concurrency introduces additional obligations, but removing threads does not make every overlapping access harmless.
Follow values rather than names
Suppose a procedure first writes output = input + 1 and then writes output = output + input. With separate locations, the second input read obtains the original input. If input and output designate the same location in a language and API that permit this overlap, the first write also changes what the later input read sees. Trace each read and write in order before simplifying the expression. Treating the two names as independent values is an assumption that requires evidence from the interface or analysis.
The same problem appears in array kernels. An output slice can overlap an input slice partially, so advancing left to right may overwrite elements that later iterations still need. Some APIs explicitly support overlap through a temporary buffer or specified copy direction; others require disjoint regions. Do not infer one contract from a similarly named operation. For native numerical libraries, document whether in-place execution is allowed, which shapes and strides it permits, and whether two views can share storage despite having different starting addresses.
Exclusivity is an optimization premise
If a compiler can establish that writing one location cannot change another live input, it may reuse an earlier read in a register or simplify dependent operations. Violating a language's aliasing contract invalidates that reasoning, even when unoptimized machine code appeared to produce the result you expected. In safe Rust, the ordinary shared-reference and exclusive-mutable-reference rules prevent conflicting access patterns. The full unsafe aliasing model is subtle and evolving; avoid replacing it with the inaccurate slogan that two pointer values may never contain the same address.
Disjoint mutable subregions are a useful constructive example. A safe slice-splitting API can expose two mutable slices whose element ranges do not overlap. The caller can then update both without claiming that one entire array is simultaneously available through conflicting mutable views. This shifts the proof into a small, checked boundary. Raw pointers require additional reasoning: creating a raw pointer does not itself provide the lifetime, alignment, initialization, or access permission needed for dereferencing it. Address equality alone cannot settle all those questions.
Interior mutability changes the mechanism
Rust's UnsafeCell provides the primitive opt-out from the usual shared-reference immutability guarantee for its contents. It does not remove the exclusivity obligations of mutable references or permit data races. Higher-level wrappers add a discipline: a RefCell checks borrow rules dynamically in a single-threaded context, while an appropriate lock or atomic type supplies particular cross-thread guarantees. Choose the wrapper according to the required operation. An atomic field does not automatically make a multi-field invariant atomic, and a lock only protects readers and writers that actually follow its protocol.
The network lab makes a limited data-dependency connection. It computes σ(wx−0.5), with separate controls for input x and weight w. Set x=1 and w=1, inspect the product and output, then change w to 2 and predict that the output increases. The graph shows that a later read of a changed weight changes the function value. It contains no mutable pointers, overlapping slices, or compiler optimization. Explain which version of a weight an inference request should observe if another task updates a real model, and why a consistent snapshot requires a stronger design than merely storing each scalar safely.
Work it through
Trace legal overlap before simplifying
In a hypothetical scalar API that permits overlap, input initially holds 3. Execute output = input + 1, then output = output + input. Compare separate storage with both names designating the same location.
With separate locations, the first write stores 4 into output while input remains 3. The second write stores 4 + 3 = 7.
With the same location, the first write replaces 3 with 4. The second expression reads 4 twice and stores 8.
The difference, 7 versus 8, comes from the access contract and ordering, not addition rounding. A Rust API using conflicting ordinary references cannot use this hypothetical execution as justification for unsafe aliasing.
A transformation using 2 × input + 1 needs a non-overlap argument for this procedure. Test cases that happen to use separate buffers do not establish that contract for every caller.
The living notebook
Explore the model
Interactive lab · network
Observe a changed dependency
Set x=1 and w=1 in the single sigmoid σ(wx−0.5), then raise w to 2. Predict an output increase from about 0.622 to 0.818. This is a dependency graph, not an aliasing checker, concurrent weight snapshot, or trainable model.
- Product · wx
- 1.2
- Logit · wx − 0.5
- 0.7
- Sigmoid output
- 0.6682
One inspectable sigmoid unit, σ(wx − 0.5). Playback traces the computation graph. There is no training, attention mechanism or hidden deep network. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forReusing a read requires knowing whether the underlying value can change between observations.
Pause & reason
Check your understanding
Your turn
Try it yourself
An eight-element mutable slice is split at index 3 using a sound splitting API. State each index range and explain whether a split at 8 is inherently invalid.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Use half-open ranges and allow an empty right-hand range.
Compare with the solution
The ranges are [0,3) and [3,8), containing three and five elements. Splitting at 8 yields [0,8) and [8,8); the empty second range does not overlap any element. The API still checks that the split lies within the slice, and callers must respect each returned slice's access rules.
Keep the idea
Take it with you
- Overlap can change sequential behavior before concurrency enters the problem.
- Compiler transformations depend on the access contracts promised by the language and API.
- Interior mutability requires a disciplined abstraction rather than unrestricted shared writes.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.