2026-06-13
Every store your CPU executes goes into the store buffer first — a small FIFO between the execution units and the L1 cache. This lets stores retire from the ROB without waiting for cache, lets younger loads bypass older stores via forwarding, and lets the cache coherence machinery work on stores in the background. The store buffer is one of the most important latency-hiding tricks in modern CPUs.
A memory fence (MFENCE on x86, DMB ISH on ARM, fence rw,rw on RISC-V) tells the CPU: no younger memory operation may become globally visible until every older one has. In practice, that means drain the store buffer before any subsequent load or store can execute against the cache.
Why is this so painful? The store buffer typically holds 40–72 entries (Skylake: 56, Zen 4: 64). If even a handful of those stores target lines that are not in M or E state in this core's L1, each one triggers a Read-For-Ownership on the ring/mesh, waiting on snoop responses from every other L1, possibly fetching from L3 or another socket. A fence stalls the entire pipeline until the slowest of those RFOs completes.
Concrete example: A naive single-producer/single-consumer queue uses std::atomic<T> with seq_cst semantics. On x86, every store compiles to MOV; MFENCE (or XCHG, which is implicitly fenced). Measure it: an unfenced store retires in ~1 cycle of pipeline cost. The same store with MFENCE stalls for 30–50 cycles when the store buffer is mostly empty — and 100+ cycles when it's full and several stores are waiting on RFOs. Switching to memory_order_release/memory_order_acquire on x86 emits no fence at all (the TSO model gives it for free) and the queue throughput jumps 5–10x.
Rule of thumb: Cost of a fence ≈ (number of pending stores to non-M-state lines) × (RFO latency). Worst case ≈ store buffer depth × L3 miss latency ≈ 64 × 200 ns ≈ microseconds, though typical is 30–200 cycles. If you're issuing a fence per operation in a hot loop, you've capped your throughput at roughly core_frequency / fence_cost — at 3 GHz with 50-cycle fences, that's 60M ops/sec, period.
This is why lock-free doesn't mean fast: replacing a mutex with a CAS loop that fences every iteration can be slower than the mutex it replaced. ARM and POWER, with their weaker memory models, make every fence explicit — which is brutally honest but lets you skip them when you don't need them, something x86's implicit ordering quietly does for you on every release-store.
seq_cst can destroy the performance of an otherwise lock-free algorithm.
