Skip to content
← Back to explainers

Rust explainer

How borrowing helps the optimizer

About 5 min to read. Allow extra time to work through examples.

See how reference guarantees can eliminate redundant reads, what LLVM attributes mean, and why interior mutability needs different assumptions.

This article uses Rust. You can make it your practice mode to browse related content across the site.

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

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.

step 23 of 24

Ownership II: Borrowing and the Borrow Checker

Up next Understanding borrow-checker errors