Question

Why Rust May Not Optimize Non-Aliasing Mutable References Yet

rustreferencecompiler-optimizationllvm-codegen

Question

In C, pointer aliasing can prevent certain optimizations because the compiler must preserve correct behavior even when two pointers refer to the same memory.

For example:

void adds(int *a, int *b) {
    *a += *b;
    *a += *b;
}

When compiled with Clang using optimizations, the compiler may reload *b and store to *a twice, because a and b might alias.

If we add restrict:

void adds(int *restrict a, int *restrict b) {
    *a += *b;
    *a += *b;
}

then the compiler can assume the pointers do not alias and optimize the function more aggressively.

In Rust, mutable references are supposed to be exclusive, so this code seems like it should allow the same optimization:

#![crate_type = "staticlib"]

#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

However, the generated machine code may still look like the non-restrict C version.

Why does this happen? Is it because the Rust compiler has not yet fully applied aliasing-based optimizations, or because there are situations where two &mut references could still alias?

Short Answer

By the end of this page, you will understand how Rust's &mut references relate to aliasing, why non-aliasing guarantees do not always become immediate machine-code optimizations, and how compiler backends like LLVM, undefined behavior rules, and code generation details affect the final result.

Concept

Rust's &mut T means exclusive mutable access: while a mutable reference is active, no other reference is allowed to access the same value in a conflicting way. Conceptually, that sounds similar to C's restrict, because both ideas help the compiler reason about aliasing.

However, there are a few important details:

&mut is a language rule, not just a syntax hint

In safe Rust, creating two simultaneously active &mut references to the same location is forbidden. If that happens through unsafe code, the program may have undefined behavior.

That means the compiler is allowed to assume Rust's aliasing rules are respected.

But optimization depends on how those rules are represented internally

Rust does not optimize code directly into assembly by itself. rustc lowers Rust code into LLVM IR, and LLVM performs many optimizations from there. For LLVM to use the non-aliasing property, Rust must communicate the right information in a form LLVM understands, such as attributes like noalias.

If that information is missing, conservative, limited to certain contexts, or not useful enough for a given optimization pass, LLVM may generate less optimized code even though the source-level rules would permit better code.

&mut and restrict are similar, but not identical concepts

They are related, but they are not a perfect one-to-one match:

  • restrict is a C promise attached to a pointer usage pattern.
  • &mut is part of Rust's reference validity and borrowing model.
  • Rust's rules are stronger in some ways, but turning them into backend-friendly optimization facts is subtle.

Why this matters

Aliasing affects:

  • whether values can be cached in registers
  • whether loads can be reused
  • whether stores can be reordered
  • whether loops can be vectorized

If the compiler cannot prove non-aliasing at the backend level, it often reloads memory more often than necessary.

The key takeaway

The lack of a specific optimization does not mean &mut may alias in safe Rust. More often, it means one of these:

  • the aliasing guarantee was not fully exploited by codegen
  • LLVM did not apply that optimization in this specific case
  • the version of rustc/LLVM being used did not propagate enough aliasing metadata
  • ABI or interop concerns caused more conservative code generation

So the issue is usually about optimization pipeline limitations, not about safe Rust permitting aliasing of two &mut references.

Mental Model

Think of &mut as giving one person the only key to a storage locker.

  • If Alice has &mut locker, nobody else is supposed to have a key at the same time.
  • Because of that, Alice can safely rearrange the contents without worrying that someone else changes them behind her back.

Now imagine Alice tells a warehouse robot to optimize her work. The robot can only act on information written on the shipping form.

  • Rust's rules say: "Alice is the only one with the key."
  • But the optimizer only sees what is encoded in the intermediate representation.
  • If that exclusivity is not fully written down in a way the robot understands, the robot behaves cautiously.

So the important distinction is:

  • language guarantee: &mut is exclusive
  • optimizer usage: the backend must actually know and exploit that fact

Rust can guarantee exclusivity at the language level even when the generated code is still conservative.

Take Quiz

Syntax and Examples

Core idea in Rust

A mutable reference gives exclusive mutable access:

fn increment(x: &mut i32) {
    *x += 1;
}

This means while x: &mut i32 is active, Rust assumes no other active reference is mutating or observing the same value in a conflicting way.

Example that suggests a non-alias optimization

fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

A human might rewrite this mentally as:

fn adds(a: &mut i32, b: &mut i32) {
    let temp = *b;
    *a += temp * 2;
}

This is valid if a and b do not alias.

Why Rust source permits this reasoning

Calling the function like this is fine:

fn main() {
    let mut x = 10;
    let mut y = 3;
    adds(&mut x, &mut y);
    println!("{} {}", x, y);
}

But calling it with two mutable references to the same value is rejected in safe Rust:

fn main() {
    let mut x = 10;
    // adds(&mut x, &mut x); // error
}

Rust prevents this because it would create overlapping &mut borrows.

Unsafe code can break the rules

unsafe fn bad(p: *mut i32) {
    let a: &mut i32 = &mut *p;
    let b: &mut i32 = &mut *p;
    *a += *b;
}

This compiles only because unsafe lets you perform operations the compiler cannot verify. But if both references are active and alias, this violates Rust's rules and can cause undefined behavior.

That is why the compiler is allowed to assume properly formed &mut references are exclusive.

Step by Step Execution

Consider this Rust function:

fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

Suppose we call it like this:

let mut x = 5;
let mut y = 2;
adds(&mut x, &mut y);

Step-by-step behavior

Initial values:

  • x = 5
  • y = 2

Inside the function:

  1. Read *a → reads x, so value is 5
  2. Read *b → reads y, so value is 2
  3. Compute 5 + 2 = 7
  4. Store 7 into *a → now x = 7
  5. Read *a again → now 7
  6. Read *b again → still 2
  7. Compute 7 + 2 = 9
  8. Store 9 into *a → now x = 9

Final values:

  • x = 9
  • y = 2

Why aliasing matters

If a and b pointed to the same memory, step 4 would also change what step 6 reads. That is exactly why compilers are careful in languages or situations where aliasing is possible.

Why the optimization would be valid in Rust

Because safe Rust does not allow a and b to be two active &mut references to the same i32, the compiler could treat *b as unchanged by writing through a.

So an optimized interpretation is:

let temp = *b;
*a += temp * 2;

If this optimization does not happen, the most likely reason is not that aliasing is allowed, but that the compiler backend did not perform or exploit that transformation in this case.

Real World Use Cases

Aliasing information matters in many real programs:

Numeric and scientific code

When processing arrays, matrices, or vectors, the compiler can generate faster code if it knows two mutable references do not overlap.

fn scale_and_add(a: &mut i32, b: &mut i32) {
    *a += *b * 4;
}

In bigger loops, this can affect vectorization and register reuse.

Parsing and data transformation

When updating one field based on another, non-aliasing assumptions help the compiler avoid unnecessary memory reloads.

Systems programming

Rust is often used where predictable performance matters. Borrowing rules provide useful guarantees that can, in principle, enable strong optimizations.

APIs with input/output buffers

A function that writes to one buffer and reads from another is easier to optimize if the compiler knows the buffers do not overlap.

In-place mutation without accidental overlap

Rust's borrowing rules help developers write APIs where mutation is explicit and aliasing bugs are prevented early, even before optimization enters the picture.

Take Quiz

Real Codebase Usage

In real Rust codebases, developers rely on the semantic guarantee of &mut more than on any single micro-optimization.

Common patterns

Guarded mutation

fn update(score: &mut i32, delta: i32) {
    if delta == 0 {
        return;
    }
    *score += delta;
}

Exclusive access makes mutation straightforward and safe.

Splitting data into non-overlapping mutable pieces

fn swap_first_two(items: &mut [i32]) {
    if items.len() < 2 {
        return;
    }
    items.swap(0, 1);
}

Libraries often provide safe APIs like split_at_mut specifically to produce two guaranteed non-overlapping mutable slices.

fn add_edges(values: &mut [i32]) {
    if values.len() < 2 {
        return;
    }

    let (left, right) = values.split_at_mut(1);
    left[0] += right[0];
}

Validation and early returns

Developers often structure functions so the exclusive mutable borrow exists only where needed, which can help readability and sometimes optimization.

Unsafe internals, safe outer API

Real libraries may use raw pointers internally for performance, but expose safe &mut APIs so callers cannot create aliasing violations.

Important practical note

Whether a particular instruction-level optimization appears can vary by:

  • Rust version
  • LLVM version
  • target architecture
  • ABI and calling convention
  • whether the function is generic, inlined, or exported

In real projects, developers usually focus first on clear borrowing and correct ownership, then inspect generated code only for performance-critical paths.

Common Mistakes

Mistake 1: Assuming missing optimization means aliasing is allowed

A function may compile to conservative machine code even when Rust's rules guarantee no aliasing.

fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

If the compiler reloads *b, that does not prove a and b may alias in safe Rust.

Mistake 2: Confusing safe Rust rules with unsafe behavior

This is invalid in terms of Rust's aliasing model, even if you can write it with unsafe:

unsafe fn broken(p: *mut i32) {
    let a = &mut *p;
    let b = &mut *p;
    *a += 1;
    *b += 1;
}

Avoid creating multiple active &mut references to the same memory.

Mistake 3: Thinking &mut and C restrict are exactly the same

They are related but come from different language models.

  • restrict is an explicit C promise.
  • &mut is part of Rust's reference validity rules.

Do not assume every backend optimization for restrict will appear identically for &mut.

Mistake 4: Benchmarking tiny functions in isolation and drawing broad conclusions

Small exported functions can compile differently from inlined code inside a larger program.

#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

Attributes like #[no_mangle] may affect inlining and optimization opportunities.

Mistake 5: Ignoring version differences

Compiler output changes over time. A result from one rustc + LLVM version may not hold for another.

Mistake 6: Using unsafe to force patterns you think are faster

Trying to outsmart the compiler with raw pointers often makes code less safe and not necessarily faster. Prefer safe code first, then profile.

Comparisons

ConceptWhat it meansCompiler impactSame as Rust &mut?
Rust &mut TExclusive mutable referenceCan permit non-alias assumptions if conveyed to backendNot exactly, but closely related
Rust &TShared immutable referenceUsually no mutation through it, but aliasing is allowedNo
C pointer int *Ordinary pointerCompiler must assume aliasing unless proven otherwiseNo
C int *restrictPromise that accesses do not alias in the relevant scopeEnables stronger optimizationSimilar in effect, not identical in model
Raw pointer *mut T in RustUnchecked mutable pointerFewer guarantees for optimization unless provenNo

&mut vs raw pointers

fn safe_ref(x: &mut i32) {
    *x += 1;
}

unsafe fn raw_ptr(x: *mut i32) {
    *x += 1;
}
  • &mut i32 carries Rust's exclusivity rules.
  • *mut i32 does not, by itself, provide the same high-level guarantee.

Source semantics vs generated code

LevelQuestion
Rust language semanticsAre two active &mut references to the same place allowed?
AnswerNo, not in safe Rust
Backend optimizationDid the compiler turn that fact into better machine code here?
AnswerNot always

That distinction is the heart of this topic.

Take Quiz

Cheat Sheet

Quick facts

  • &mut T means exclusive mutable access.
  • In safe Rust, two active &mut references to the same location are not allowed.
  • If unsafe code creates aliasing &mut references, behavior can be undefined.
  • A missing optimization does not mean aliasing is permitted.
  • rustc relies on LLVM for many low-level optimizations.
  • LLVM can optimize better only if aliasing information is correctly represented and used.

Mental shortcut

  • Language rule: &mut is exclusive.
  • Codegen reality: exclusivity must survive lowering into optimizer-friendly metadata.

Typical valid reasoning

fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

Because a and b cannot alias in safe Rust, this is semantically equivalent to:

fn adds(a: &mut i32, b: &mut i32) {
    let t = *b;
    *a += t * 2;
}

But equivalent source semantics do not guarantee identical assembly in every compiler version.

Watch out for

  • unsafe code that creates overlapping &mut
  • drawing conclusions from one compiler version
  • assuming #[no_mangle] and exported functions optimize like inlined private functions
  • equating Rust references exactly with C restrict

Useful related APIs

  • split_at_mut for safely creating disjoint mutable slices
  • borrowing rules and lifetimes
  • raw pointers for low-level interop

FAQ

Does Rust guarantee that two &mut references cannot alias?

Yes, in safe Rust. If aliasing &mut references are created through unsafe code, that violates Rust's rules and may cause undefined behavior.

If &mut is exclusive, why doesn't the compiler always optimize more aggressively?

Because the optimization depends on whether that exclusivity is preserved and exploited in the compiler pipeline, especially in LLVM IR and later passes.

Is Rust's &mut the same as C's restrict?

Not exactly. They are similar in optimization intent, but they come from different language semantics and are not a direct one-to-one feature match.

Does unsafe mean aliasing &mut references becomes valid?

No. unsafe allows you to perform unchecked operations, but it does not make invalid aliasing legal. It just makes it your responsibility.

Can compiler versions change this behavior?

Yes. Code generation and optimization can differ across rustc and LLVM versions.

Does #[no_mangle] affect optimization?

It can affect how the function is treated, especially around visibility, linkage, and inlining opportunities.

Should I rewrite safe Rust into raw pointers for performance?

Usually no. Start with safe Rust, profile first, and only use unsafe when you have a proven need and can maintain correctness.

Take Quiz

Mini Project

Description

Build a small Rust example that demonstrates how Rust prevents aliasing of mutable references in safe code, while still allowing efficient mutation of separate values. This project helps you connect borrow rules with optimization-friendly semantics.

Goal

Write a program that safely updates two separate integers through &mut references and shows a borrow-checker error when trying to alias them in safe Rust.

Requirements

  1. Create a function that takes two &mut i32 parameters and updates one using the other.
  2. Call the function with two different variables and print the result.
  3. Include a commented-out example showing that passing &mut x twice is rejected.
  4. Add a second function that uses a temporary variable to make the non-aliasing logic explicit.
Take Quiz

Keep learning

Related questions

View more