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.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
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.
| Claim | True? | What is actually the case |
|---|---|---|
| Orders this core's operations before it against those after it | Yes | This is the definition, and the whole of it |
| Flushes the cache or writes back dirty lines | No | Cache contents are untouched; coherence already handles visibility |
| Makes pending writes reach other cores sooner | No | Drain speed is unchanged — the core waits, the memory system does not hurry |
| Provides mutual exclusion | No | No exclusivity of any kind; that requires an atomic read-modify-write |
| One core's barrier fixes another core's missing ordering | No | Ordering needs a matched pair — a release on one side, an acquire on the other |
Every ISA spells it differently
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.
| Architecture | Full barrier | Narrower forms | Note |
|---|---|---|---|
| x86-64 | MFENCE, or any LOCK-prefixed instruction | SFENCE, LFENCE | Only Store→Load needs fencing, so full barriers dominate in practice |
| AArch64 | DMB ISH | DMB ISHLD, DSB, ISB | DSB waits for completion; ISB flushes the instruction pipeline — a different job |
| RISC-V | FENCE rw,rw | FENCE r,r and other predecessor/successor sets | Ordering sets are named explicitly in the encoding |
| C++ / Rust source | atomic_thread_fence(seq_cst) | acquire, release, acq_rel | The 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.
1// Producer // Consumer2data = compute(); while (!ready) { }3full_fence(); use(data);4ready = true;5 6// The producer's stores are ordered. The consumer's7// loads are not: on AArch64 the load of 'data' may be8// issued before the load of 'ready' resolves.9// Still broken, and now it looks defended.1// Producer // Consumer2data = compute(); while (!ready.load(acquire)) { }3ready.store(true, release); use(data);4 5// x86-64: both compile to plain MOV — the ordering is6// already architectural, cost is zero7// AArch64: STLR and LDAR — dedicated ordered forms,8// cheaper than a general DMB9// 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.
- 1Barrier instruction → core: the core stops allowing later memory operations to become visible ahead of earlier ones.
- 2Core → store buffer: for a full barrier, pending stores must reach a defined point in the coherence order before the core proceeds.
- 3Coherence → other cores: those stores are now observable; nothing was made faster, the core simply waited for it.
- 4Later operations → memory system: operations after the barrier may now proceed, and cannot be observed ahead of the earlier ones.
- 5Consumer core → its own barrier: independently, the consumer's acquire prevents its later loads from being hoisted above the flag load.
- • "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.
- • "
MFENCEandDMB ISHare 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
- • 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.
- • 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".
- • 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.
- • 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.
- ISA-SPECIFICBarrier instructions, their granularity and their guarantees differ per architecture: x86-64
MFENCE, AArch64DMB/DSB/ISB, RISC-VFENCEwith 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
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
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.