Two pointers alias when they refer to overlapping memory. Aliasing matters when a write through one pointer can change a later read through another. If the compiler can rule out that change, it may reuse a value already read.
Rust references carry access rules that help establish this. Safe borrowing prevents conflicting accesses, and unsafe code must uphold the same rules. They apply even when optimization is disabled or the generated code appears to work with an invalid reference.
A write that cannot change the input
Consider a function that updates one integer based on another. The example
below uses wrapping multiplication so every u32 value has defined behavior,
including values that overflow when doubled.
fn compute(input: &u32, output: &mut u32) {
if *input > 10 {
*output = 1;
}
if *input > 5 {
*output = output.wrapping_mul(2);
}
}
fn compute_cached(input: &u32, output: &mut u32) {
let value = *input;
if value > 10 {
*output = 2;
} else if value > 5 {
*output = output.wrapping_mul(2);
}
}
fn main() {
for input in [0, 5, 6, 10, 11, u32::MAX] {
for initial in [0, 7, u32::MAX] {
let mut original = initial;
let mut cached = initial;
compute(&input, &mut original);
compute_cached(&input, &mut cached);
assert_eq!(original, cached);
}
}
}
For these calls, input and output refer to separate integers. Writing
through output cannot change input. Both functions therefore have the same
result:
| Input value | Final output |
|---|---|
0..=5 |
Initial output unchanged |
6..=10 |
Initial output doubled with wrapping arithmetic |
11..=u32::MAX |
2 |
The second function shows a transformation the compiler may make. It reads
the input once and avoids writing 1 before overwriting it with 2.
It still contains two comparisons on one path. Neither source spelling
guarantees an exact number of loads, branches or stores in machine code.
You cannot test an overlapping Rust call by constructing &x and &mut x
and using both in this function. Safe Rust rejects it. Manufacturing the
conflicting references with unsafe code does not make the call valid.
What changes with C pointers?
A C function taking const unsigned *input and unsigned *output can allow
both pointers to address the same object. If that object initially holds 20,
the first assignment sets it to 1. The second comparison then reads 1,
so the function leaves the result at 1. Unconditionally replacing that path
with the cached version would change the result to 2.
Check your understanding Does that mean a C compiler must emit every read and write exactly as written? Show answer Hide answer
No. It must preserve the behavior of permitted calls, including overlapping pointers, but it can use other evidence to optimize. Inlining may reveal distinct objects; analysis may prove non-overlap; generated code may choose different paths after checking addresses.
const prevents writes through that pointer expression. It does not establish
that another pointer cannot modify the object. C's restrict can provide an
additional access promise, with requirements the caller must satisfy. It is
one source of optimization information, not the only one.
Read compiler output as evidence, not a language contract
LLVM represents some pointer guarantees with attributes. Rust's LLVM backend can attach them to reference parameters. Examples include:
noalias: an access-based non-aliasing promise for the relevant execution.readonly: the parameter is not used to write through that pointer.dereferenceable(N): a specified number of bytes can be read safely under the attribute's conditions.align(N): a pointer alignment requirement.
These have precise meanings in the LLVM language reference.
noalias does not mean that no other pointer with the same address can exist,
and readonly alone does not mean nobody else can write to the memory.
The attributes depend on the type, compiler version, backend and optimization
pipeline; there is no universal attribute list for every Rust reference.
To inspect a particular build, save the Rust example as aliasing.rs and run:
rustc --edition=2024 -O --emit=llvm-ir aliasing.rs
The helper functions may disappear after inlining. For a focused experiment,
make them public library functions and compile with --crate-type=lib.
Record the compiler version and target when comparing results. Inspecting IR
or assembly can show an optimization; a timing difference alone cannot explain
which transformation caused it.
Opportunities, not automatic speedups
Knowing which accesses can overlap can help a compiler reuse loaded values, remove unobserved stores, move independent operations or vectorize a loop. Other constraints still matter, including arithmetic semantics, side effects and the target instruction set.
This does not establish that Rust is always faster than C, or that particular language costs cancel each other out. Measure the actual workload. Allocation, cache behavior, input size and algorithm choice may matter more than alias analysis. Bounds checks can sometimes be removed, and moves do not inherently require copying a buffer's contents.
Where interior mutability fits
UnsafeCell<T> allows its contents to change through shared access. Types such
as Cell and RefCell build safe APIs around that primitive. The compiler
must account for this possibility instead of treating those contents as frozen
merely because access began through a shared reference.
It does not permit conflicting uses of &mut T, and it does not prevent data
races. Each abstraction still needs rules for safe access: RefCell checks
borrows at runtime, while synchronization types coordinate concurrent access.
Casting a shared reference to a raw pointer does not by itself grant permission
to mutate ordinary data behind it.
See the UnsafeCell documentation for its precise contract. The Rustonomicon's aliasing discussion develops the optimization example further. Continue with the borrow checker as a proof system to connect these guarantees to everyday borrowing decisions.