Memory Barriers in Modern CPUs

Memory barriers (also called fences) exist because CPUs and compilers reorder memory operations for performance, and in multi-core systems this reordering can become visible to other cores in surprising ways.

Barriers are instructions that restrict this reordering across a specific point in the instruction stream.

1. Why reordering happens

Modern CPUs use several tricks that can change the order in which memory operations appear to complete from another core's perspective:

  • Store buffers: a core writes to a local buffer immediately and drains it to cache/memory later, so a store can be delayed relative to later instructions.
  • Out-of-order execution: independent instructions execute in whatever order resources allow.
  • Speculative loads: loads may execute early, before it's certain they should have.
  • Cache coherence protocol delays: invalidations/updates from other cores don't arrive instantaneously.
  • Compiler reordering: the compiler itself may reorder or eliminate memory accesses absent barriers.

On a single core, this is invisible — the core always sees its own operations in program order (this is enforced automatically). The problem shows up only when multiple cores observe each other's memory operations.

2. The four basic orderings

3.1 LoadLoad barrier

Prevents loads before the barrier from being reordered with loads after it.

  Load A
  LoadLoad barrier <-- the "Load-Load" barrier
  Load B   // guaranteed to appear after Load A

3.2 StoreStore barrier

Prevents stores before the barrier from being reordered with stores after it. This is important because store buffers can drain out of order.

  Store A
  StoreStore barrier <-- the "Store-Store" barrier
  Store B   // other cores never see B before A

3.3 LoadStore barrier

Prevents a load before the barrier from being reordered with a store after it (the load must complete before the store is issued).

  Load A
  LoadStore barrier
  Store B

3.4 StoreLoad barrier

Prevents a store before the barrier from being reordered with a load after it. This is the most expensive barrier on most architectures, because it typically requires the store buffer to be fully drained before the following load can proceed — it's the one ordering that store buffers naturally violate.

  Store A
  StoreLoad barrier <-- the "Store-Load" barrier
  Load B

Note: This last one matters a lot in practice: it's what's needed for patterns like Dekker's algorithm or double-checked locking, where a thread writes a flag and then needs to read another thread's flag and be sure it sees the up-to-date value.

3. How real architectures expose these

Different ISAs give you different granularity:

3.1 x86 (TSO — Total Store Order)

x86 already forbids most reorderings except StoreLoad (due to store buffers). So you rarely need explicit LoadLoad/LoadStore/StoreStore fences — the hardware gives you those for free. You mainly need:

  • mfence — full barrier (all four orderings, notably StoreLoad)
  • sfence — orders stores only (mostly relevant with non-temporal/streaming stores)
  • lfence — orders loads only (also used for speculation control, e.g. Spectre mitigations)
  • A lock-prefixed instruction (like lock xchg) is a full barrier as a side effect, and is often cheaper than mfence in practice.

3.2 ARM / POWER (weakly-ordered)

These architectures reorder much more aggressively by default, so you get fine-grained barrier instructions:

  • ARM:
    • dmb (data memory barrier): general purpose fence, with variants for full/store-only ordering, and inner/outer shareability domains.
    • dsb (stronger): also waits for completion of prior memory accesses, used more for system-level sync.
    • isb (instruction sync): flushes the pipeline, relevant for self-modifying code or after changing CPU state.
  • ARM also has acquire/release variants of loads and stores (ldar/stlr) which bake ordering semantics directly into the instruction rather than requiring a separate fence — this is often cheaper than a full dmb.
  • POWER: sync (full barrier, aka hwsync), lwsync (lightweight sync — omits StoreLoad ordering, cheaper), isync (instruction sync, similar role to ARM's isb).

3.3 RISC-V

Uses a single flexible fence instruction, parameterized by predecessor/successor sets (fence PR, PW, SR, SW — predecessor read/write, successor read/write), which lets you encode any of the four barrier types (or combinations) in one instruction.

4. Acquire/release: the abstraction most programmers actually use

Rather than reasoning in terms of Load/Store barrier combinations directly, most concurrent code (and languages like C++, Java, Rust) exposes acquire and release semantics:

  • Acquire (on a load): no subsequent memory operation may be reordered before this load. Used when entering a critical section or reading a "ready" flag — you want everything after to see up-to-date data.
  • Release (on a store): no prior memory operation may be reordered after this store. Used when exiting a critical section or publishing data — you want everything before to be visible before the flag flips.

An "acquire load" paired with a "release store" on the same variable, across two threads, creates a happens-before relationship:

  • everything the releasing thread did before its store becomes visible to everything the acquiring thread does after its load.

This is exactly the pattern behind mutexes, std::atomic with memory_order_acquire / release, Java's volatile, etc.

Note: A full barrier (sometimes called sequentially consistent) is stronger than either alone — it prevents reordering in both directions and is needed for things like Dekker-style mutual exclusion without locks.

Comments

Popular posts from this blog

Recent Innovations in Ergonomic Split Designs

A Smarter Way to Type: Ergonomic Mechanical Keyboards

Traveling on a Budget: Tips for Affordable Adventures