Orderingmemory barrierfenceacquirereleasemfencedmb

Memory Barriers: Ordering, Not Flushing

A barrier is one of the most misdescribed instructions in computing. It does not flush caches, it does not push data anywhere, and it does not lock anything. It constrains the order in which one core's memory operations may become visible relative to each other.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
What does a memory barrier actually constrain, and why is "it flushes the write buffer to memory" the wrong mental model?
What you wrote
A barrier is a checkpoint that pushes all my pending writes out to memory so everyone can see them, and stops anything from crossing it.
What the hardware does
A barrier establishes an ordering relation between the memory operations before it and those after it, as observed by other cores. It does not move data, does not evict cache lines, and constrains only the core that executes it.
The flush model predicts wrong behaviour in both directions. It suggests barriers make writes visible sooner — they do not, they only make this core wait — and it suggests one core's barrier can fix another core's missing one, which is precisely the bug in most hand-rolled synchronisation.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

What a barrier does and does not do

The precise statement is relational: a barrier guarantees that memory operations appearing before it in program order become visible to other observers before any memory operation appearing after it. That is the entire guarantee. Everything else people attribute to barriers is imported from a mental model of caches that does not apply.

In particular a barrier does not accelerate anything. As Store Buffers: Where Your Writes Wait describes, pending stores were already draining as fast as the coherence protocol allowed. A fence makes the executing core wait until that draining has reached a defined point. You are buying ordering by spending cycles, not by making the memory system work harder.

It is also strictly one-sided. A barrier on the producer orders the producer's own operations. If the consumer performs its loads in the wrong order — which on AArch64 it may — the producer's barrier does nothing to help. Ordering requires a matched pair, one on each side, which is exactly what an acquire/release pair expresses and what hand-rolled single-sided fences habitually get wrong.

What a memory barrier does, and the four things it is routinely believed to do
ClaimTrue?What is actually the case
Orders this core's operations before it against those after itYesThis is the definition, and the whole of it
Flushes the cache or writes back dirty linesNoCache contents are untouched; coherence already handles visibility
Makes pending writes reach other cores soonerNoDrain speed is unchanged — the core waits, the memory system does not hurry
Provides mutual exclusionNoNo exclusivity of any kind; that requires an atomic read-modify-write
One core's barrier fixes another core's missing orderingNoOrdering needs a matched pair — a release on one side, an acquire on the other

Every ISA spells it differently

ISA-SPECIFICInstruction names and their exact guarantees are per-architecture. An `MFENCE` and a `DMB ISH` are not equivalent, and neither maps one-to-one onto a C++ fence.

There is no portable barrier instruction and no portable set of barrier *kinds*, which is the strongest practical argument for expressing ordering in the language rather than in assembly. The names, the granularity and the exact guarantees differ substantially between architectures.

x86-64 provides MFENCE, SFENCE and LFENCE. Because the architecture only permits Store→Load reordering, the store and load fences are rarely what you want for ordinary shared memory, and MFENCE is the general tool. In practice any LOCK-prefixed instruction also acts as a full barrier, so a LOCK XCHG to a scratch location has long been a common — and on many parts cheaper — idiom than MFENCE itself.

AArch64 provides DMB for data memory ordering, DSB for completion (stronger, waits for the operations to complete rather than merely be ordered), and ISB for instruction-stream synchronisation, each with domain and direction qualifiers such as ISH and ISHLD. RISC-V takes a different approach again, with a single FENCE instruction carrying explicit predecessor and successor sets. Mapping "I want a barrier" onto these correctly per target is a job for a compiler, not a human.

Barrier facilities by architecture. Not equivalents — the guarantees genuinely differ.
ArchitectureFull barrierNarrower formsNote
x86-64MFENCE, or any LOCK-prefixed instructionSFENCE, LFENCEOnly Store→Load needs fencing, so full barriers dominate in practice
AArch64DMB ISHDMB ISHLD, DSB, ISBDSB waits for completion; ISB flushes the instruction pipeline — a different job
RISC-VFENCE rw,rwFENCE r,r and other predecessor/successor setsOrdering sets are named explicitly in the encoding
C++ / Rust sourceatomic_thread_fence(seq_cst)acquire, release, acq_relThe compiler emits the correct per-target encoding, or nothing at all where none is needed

Acquire and release: the abstraction worth using

Standalone fences are hard to place correctly because they order *everything* around them, which is both more than you need and easy to get wrong by one line. The abstraction that has won is to attach ordering to the synchronising operation itself: a release store orders everything before it against that store, and an acquire load orders that load against everything after it.

Paired, they create exactly the edge the publish pattern needs and nothing more. The producer's release store guarantees the initialised data is visible before the flag; the consumer's acquire load guarantees the flag is read before the data. The pairing is the point — either half alone is as broken as no barrier at all, which is the single most common defect in hand-rolled lock-free code.

The encoding advantage is significant and free. On x86-64, where loads and stores are already ordered, an acquire load and a release store frequently compile to a plain MOV with no fence whatsoever — the ordering is architecturally guaranteed. On AArch64 they compile to LDAR and STLR, dedicated ordered instructions that are cheaper than a general DMB. The same source gets the cheapest correct encoding on both, which is not something a hand-written fence can achieve.

Single-sided: a fence on the producer only
1// Producer // Consumer
2data = compute(); while (!ready) { }
3full_fence(); use(data);
4ready = true;
5
6// The producer's stores are ordered. The consumer's
7// loads are not: on AArch64 the load of 'data' may be
8// issued before the load of 'ready' resolves.
9// Still broken, and now it looks defended.
Paired: release on the store, acquire on the load
1// Producer // Consumer
2data = compute(); while (!ready.load(acquire)) { }
3ready.store(true, release); use(data);
4
5// x86-64: both compile to plain MOV — the ordering is
6// already architectural, cost is zero
7// AArch64: STLR and LDAR — dedicated ordered forms,
8// cheaper than a general DMB
9// One source, cheapest correct encoding per target.

The pair, not the fence, is what creates ordering. A release without a matching acquire orders the producer against nothing the consumer is obliged to respect. Expressing it as acquire/release also lets the compiler emit zero instructions where the architecture already guarantees the ordering — an optimisation no hand-placed fence can express.

Key points

  • A barrier constrains the order in which this core's memory operations become visible; it moves no data and touches no cache.
  • It does not make writes visible sooner — it makes the executing core wait until they already are.
  • Ordering requires a matched pair: a one-sided fence orders the producer against nothing the consumer must honour.
  • Barrier instructions and their guarantees are per-ISA, with no portable equivalents, which is why the language abstraction is the right level.
  • Acquire/release attaches ordering to the synchronising operation and compiles to nothing at all on x86-64 where the ordering is already guaranteed.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Barrier instruction → core: the core stops allowing later memory operations to become visible ahead of earlier ones.
  2. 2
    Core → store buffer: for a full barrier, pending stores must reach a defined point in the coherence order before the core proceeds.
  3. 3
    Coherence → other cores: those stores are now observable; nothing was made faster, the core simply waited for it.
  4. 4
    Later operations → memory system: operations after the barrier may now proceed, and cannot be observed ahead of the earlier ones.
  5. 5
    Consumer core → its own barrier: independently, the consumer's acquire prevents its later loads from being hoisted above the flag load.
What people conclude from this — wrongly
  • "The fence flushed my writes so the other thread will see them" — visibility was already in progress; the fence only made this core wait for it.
  • "I put a barrier in, so this is now thread-safe" — barriers order, they do not exclude. Mutual exclusion needs an atomic read-modify-write.
  • "MFENCE and DMB ISH are the same instruction with different names" — they carry genuinely different guarantees on genuinely different memory models.
  • "Acquire/release is weaker than a full fence, so it is less safe" — it is more precise. It orders exactly the pair you named, which is what correctness requires and all it requires.

Consequences, controls and cost

What it causes
  • • A producer-side-only fence produces code that is broken on AArch64 while appearing correct on x86-64, which is a particularly expensive failure mode.
  • • Fence cost is highly variable — cheap with an empty store buffer, expensive when pending stores target contended lines — so isolated microbenchmarks mislead.
  • • Over-fencing in a hot loop can dominate its runtime while providing ordering nothing required.
  • • Hand-written fences are frequently redundant on x86-64 and insufficient on AArch64 at the same time.
What you can do
  • • Express ordering as acquire/release on the atomic operation rather than as standalone fences — it is more precise, portable and often free.
  • • Always pair: every release needs the acquire that observes it, or you have ordered one side against nothing.
  • • Reserve standalone fences for the rare patterns that genuinely need them, such as sequentially consistent fences in Dekker-style algorithms.
  • • Read the emitted assembly to see what your ordering actually cost on each target — on x86-64 the answer is frequently "no instruction at all".
How to see it
  • • Disassemble a release store on x86-64 and on AArch64 and compare: frequently a plain `MOV` on one and `STLR` on the other.
  • • Benchmark the same fence with an empty store buffer and with a buffer full of stores to contended lines to see the cost spread.
  • • Remove a fence and run the corresponding litmus test on weakly-ordered hardware to confirm it was load-bearing rather than cargo-culted.
  • • Count the fences the compiler actually emitted for a hot loop before assuming they are the bottleneck.
What it costs
  • • Stronger ordering than you need costs cycles on every execution, and on weakly-ordered targets those cycles can dominate a tight loop.
  • • Weaker ordering than you need is a correctness bug with no reliable test, which is a far worse trade than the cycles.
  • • Standalone fences are more flexible than acquire/release and correspondingly easier to place incorrectly.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • ISA-SPECIFICBarrier instructions, their granularity and their guarantees differ per architecture: x86-64 MFENCE, AArch64 DMB/DSB/ISB, RISC-V FENCE with explicit ordering sets.
  • MICROARCH-SPECIFICThe cost of a given fence depends on store-buffer occupancy and on whether pending lines are contended, so it varies between implementations and between runs.

Misconceptions

Claim
“A memory barrier flushes the CPU cache.”
Reality
It does not touch cache contents. Coherence already guarantees other cores will see the data; the barrier only constrains the order in which this core lets its operations become visible.
Claim
“A barrier on the writer is enough to publish data safely.”
Reality
Ordering is a pairwise relation. Without an acquire on the reader, the reader's loads may be reordered on any weakly-ordered machine, and the writer's barrier is irrelevant to that.
Claim
“Acquire/release is a software abstraction with no hardware meaning.”
Reality
It maps directly onto real instructions — LDAR and STLR on AArch64 — and onto the absence of instructions on x86-64, where the ordering is already architecturally guaranteed.

Where the rest of this lives

Concurrency & Parallelism
Acquire/release as a happens-before edge

This lesson covers what the instructions constrain. Using a release/acquire pair to build a correctness argument — establishing happens-before and proving an algorithm is race-free — belongs to concurrency.