Skip to content

Memory ordering

Scope: the module overview already named reordering, atomics, fences, and acquire/release in brief. This page goes one layer deeper on why reordering happens at all, what a fence actually buys you, and why the single-writer principle from the concurrency page often lets you avoid needing either.

TL;DR — in 30 seconds:

  • Compilers and CPUs reorder reads/writes for speed (store buffers, out-of-order execution) — invisible to the reordering core, but not to a second core watching.
  • Atomics guarantee an operation is indivisible, not that surrounding operations stay in order — that's what fences and acquire/release are for.
  • Fences aren't free even with zero contention (one benchmark went ~300 ms → ~4,700 ms from a single barrier); single-writer often lets you skip them because there's no second writer to order against.

1. Why "the order you wrote it" isn't the order it happens

Write A, then write B. Program order says A happens first — but neither your compiler nor the CPU promises that to anything else watching. Both are free to reorder reads and writes for speed, as long as this core, watching only its own memory, can't tell the difference. The compiler reorders to keep values in registers longer and to pipeline instructions better; the CPU reorders through out-of-order execution (dispatching whichever ready instruction it can, not necessarily the next one in sequence) and through a store buffer (a small queue that lets a write sit briefly before it becomes visible to other cores, so the writing core doesn't stall waiting for it to land).

Both reorderings are invisible to the core doing them. They stop being invisible the moment a second core is watching — because nothing in the hardware promises that core sees your writes in the order you issued them.

flowchart TB
  subgraph CORE0["Core 0 — program order: write A, then write B"]
    W1["write A"] -.->|store buffer,<br/>may delay| M["main memory / cache"]
    W2["write B"] -->|reaches memory first| M
  end
  CORE1["Core 1 — reads B, then A"] -->|sees B updated,<br/>A still stale| CORE0

2. Atomics: safe under interleaving, not ordering-safe by themselves

An atomic operation (compare-and-swap, atomic add/increment) guarantees that the operation itself is indivisible — no other core can observe it half-done, and two cores racing to CAS the same location can't corrupt it into some third value. That solves the write/write race the concurrency page described.

It does not, by itself, say anything about the operations around it. An atomic increment on variable X tells you nothing about whether a plain, non-atomic write to variable Y that happened just before it is visible to another core by the time that core observes the increment. Atomicity is a per-operation guarantee; ordering between different operations is a separate concern — which is exactly what fences and acquire/release exist to address.

3. Fences and barriers: paying to restore an order

A fence (or memory barrier) is an explicit instruction that tells the CPU and compiler: do not reorder memory operations across this point. Depending on the kind of fence, it can block reads from moving earlier, writes from moving later, or both.

Fences are not free. They give up performance the hardware would otherwise use — the store buffer stops absorbing delayed writes across the fence, and out-of-order execution loses reordering opportunities around it. In a single-threaded benchmark with zero contention, adding one barrier around a counter increment took the operation from roughly 300 ms to roughly 4,700 ms — over 15× slower, with no other thread even touching the data. A fence's cost is paid whether or not the ordering it enforces was ever at risk of mattering for that particular run.

4. Acquire/release: ordering a handoff, not the whole timeline

Rather than a blanket "don't reorder anything," acquire and release semantics order one specific kind of event: a handoff from one thread to another.

Semantic What it blocks Typical use
Release (on a write) Stops earlier operations from moving later, past this write The producing thread finishes writing data, then releases a flag/pointer that says "it's ready"
Acquire (on a read) Stops later operations from moving earlier, before this read The consuming thread acquires that same flag/pointer, then reads the data it guards

Paired together, a release-write followed by an acquire-read on the same location guarantees that everything the writer did before the release is visible to the reader after the acquire — without ordering anything else in either thread's timeline. That's a much narrower, cheaper promise than a full fence: it orders exactly the one handoff that matters, not every memory operation around it.

sequenceDiagram
  participant P as Producer thread
  participant F as shared flag
  participant C as Consumer thread
  P->>P: write payload (not yet visible)
  P->>F: release-write flag = ready
  C->>F: acquire-read flag
  Note over C: sees flag == ready
  C->>C: read payload — guaranteed visible

5. Why single-writer often needs neither

The concurrency page's single-writer principle pays off again here. Fences and acquire/release both exist to make a handoff between writers — or between a writer and a racing reader — safe and visible in the right order. If a piece of state has exactly one thread that ever writes it, there is no second writer to hand off to, and no write/write reordering hazard to fence against in the first place.

That doesn't make ordering vanish everywhere — a single-writer design still needs some mechanism (a queue, a release/acquire pair, a lock-free ring buffer) to hand finished work to consumer threads safely. But it collapses the problem from "order every contended access across every writer" down to "order one handoff path out of the owning thread," which is precisely why single-writer systems tend to need far fewer, far cheaper barriers than systems built on shared mutable state guarded by ad hoc ordering.

6. How this connects

  • This is the direct sequel to the concurrency page's two fixes: a lock enforces ordering by making threads wait their turn; single-writer avoids the need for most ordering enforcement by construction. Fences and acquire/release are the primitives you reach for when neither a full lock nor a pure single-writer design fits — ordering a narrow handoff without serializing everything.
  • It also depends on the memory-hierarchy page's store-buffer and cache mechanics: reordering is only possible because a write can sit in a buffer or a core-local cache before every other core observes it.
  • The module's exercise — sketching a shared counter unsynchronized, behind a lock, and via single-writer — is the concrete version of this trade-off: the single-writer sketch is the one that needs no fence at all, because there is only ever one writer to order against.

Common gotchas

  • Assuming an atomic operation orders everything around it. Fix: atomicity only guarantees the operation itself is indivisible — a plain write just before it can still be reordered relative to it; use a fence or acquire/release to order the surrounding operations.
  • Reaching for a full fence when only one handoff needs ordering. Fix: acquire/release orders exactly one producer→consumer handoff at a fraction of a full fence's cost — save full fences for when you actually need to block reordering broadly.
  • Assuming a fence's cost only shows up under real contention. Fix: the cost is paid unconditionally, even single-threaded with zero contention — it comes from giving up store-buffer and out-of-order optimizations at that point, not from waiting on another thread.
Check yourself — why can a second core see Core 0's write to B before its write to A, even though Core 0 wrote A first in program order?

Because the store buffer can let B's write reach memory before A's delayed write does — a reordering invisible to Core 0 itself but visible to any other core watching, since nothing in the hardware promises writes become visible to others in program order.

Check yourself — a single-writer design still needs to hand finished work to consumer threads. Does that mean it needs no ordering at all?

No — it still needs some mechanism (a release/acquire pair, a queue, a lock-free ring buffer) to order that one handoff safely. What it avoids is ordering every contended access across multiple writers, not ordering entirely.

You can defend this when you can explain why reordering happens (store buffers, out-of-order execution) even though each core's own view stays consistent; say what a fence actually costs and why that cost is paid even with zero contention; explain what acquire/release orders that a plain atomic doesn't; and say why a single-writer design needs far fewer fences than a shared-mutable-state design.