core23 min studyLesson 123 in Engineering

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

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.

yseparate=2x0+1,yoverlapping=2(x0+1)y_{\rm separate}=2x_0+1,\qquad y_{\rm overlapping}=2(x_0+1)
These results describe the two sequential assignments stated in the text, using exact scalar arithmetic and no overflow. The second formula assumes a language and API in which both names may legally denote the same mutable location; it is not permission to violate Rust reference rules.↔ Scroll wide equations horizontally.

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.

RA∩RB=∅,RA=[0,k),RB=[k,n),0≤k≤nR_A\cap R_B=\varnothing,\qquad R_A=[0,k),\quad R_B=[k,n),\quad 0\leq k\leq n
R_A and R_B are element-index ranges for a slice split at k, with integer n and k. Their disjointness supports independent element access; it does not by itself prove validity of arbitrary pointer constructions, metadata access, or concurrent algorithms.↔ Scroll wide equations horizontally.

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.

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.

  1. With separate locations, the first write stores 4 into output while input remains 3. The second write stores 4 + 3 = 7.

  2. With the same location, the first write replaces 3 with 4. The second expression reads 4 twice and stores 8.

  3. 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.

The result

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.

Explore the model

Interactive lab

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.

Observe a changed dependencyOne inspectable sigmoid unit, σ(wx − 0.5). Playback traces the computation graph. There is no training, attention mechanism or hidden deep network. Current readings: Product · wx: 1.2; Logit · wx − 0.5: 0.7; Sigmoid output: 0.6682. All parameters are adjustable with the labeled controls below.One input. One weight. One sigmoid.x1.20× w1.20+ b0.70σ0.668output0.668w = 1.00b = −0.50σ(z) = 1 / (1 + exp(−z))
Computation graphSignal trace
Product · wx
1.2
Logit · wx − 0.5
0.7
Sigmoid output
0.6682
t = 0 s

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.

Check your understanding

What does placing a Rust value inside UnsafeCell establish by itself?

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.

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.