C++ Memory Orders on the Metal: Acquire & Release Semantics Explained
Moving past abstract C++ to explore Write Combining Buffers, Prefetch Invalidation and how Intel vs. ARM handle atomics under the hood
Here’s a puzzle. This line of C++
flag.store(true, std::memory_order_release);
Compiles to a plain mov on an Intel chip and to a special stlr instruction on ARM. Same source, same semantics in the standard, completely different machine code. Why?
The C++ memory model gives you a portable contract: write release/acquire and the language promises your data transfer is safe. This article is about the machinery that makes that contract nearly free on x86 and explicit on ARM; the store buffers, speculative loads, and coherence traffic the standard deliberately hides from you.
Who this is for: you’ve used std::atomic, you've heard "acquire/release" and nodded, and you want to know what the hardware is actually doing. We'll work bottom-up through three layers: the C++ contract, the microarchitecture that implements (and sometimes violates) memory ordering, and the x86-vs-ARM split that explains the codegen above.
A note on vocabulary. I’ll use the names hardware folks use, and flag the two places I’m using my own shorthand. Skim this list now, refer back as needed:
- Architectural Registers — the named, fast core-local storage (
EAX,X0) where active computation happens. - Store Buffer (SB) — the per-core FIFO that holds committed writes before they reach memory.
- The Coherence Fabric — the interconnect that keeps every core’s view of memory in sync. (My shorthand: “the coherent system.”)
- Memory-Order Machine Clear — the pipeline squash-and-replay that recovers when a speculative load read stale data.
1. The Core Software Contract
In multi-threaded C++, std::memory_order_release and std::memory_order_acquire act as an asymmetric pair to safely transfer data visibility between threads without the heavy overhead of OS-level mutex locks.
std::memory_order_release(The Publisher): Applied to an atomic store (write). It guarantees that any memory operations occurring before this store cannot be reordered after it.std::memory_order_acquire(The Consumer): Applied to an atomic load (read). It guarantees that any memory operations occurring after this load cannot be reordered before it.
2. Hardware Mechanics: SB and Arch Registers
To understand execution sequencing, data flow within a single CPU core must be tracked across three distinct physical layers:
Arch Registers → Store Buffer(SB) →The Coherent System
[ CPU Core Execution Engine ]
| ^
| (Out-of-order Store) | (Eager Speculative Look-ahead)
v |
[ Store Buffer ] [ Speculative Load/Prefetch Buffers ]
| ^
+------------> [ The Coherent System ] --------------+
The Release Store as an Ordered SB Flush (ARM64)
Moving data from arch registers directly out to the coherent system is slow. To prevent execution stalls, the CPU dumps writes into an asynchronous store buffer (SB). On weakly ordered architectures like ARM64, the SB is naturally out-of-order. It can drain a later flag store to the coherent system before an earlier data payload store.
- The Mechanic: A release store (
stlron ARM64) acts as a strict sequential gatekeeper. It locks itself in place and forces the SB to completely flush and drain all chronologically prior writes out to the coherent system before the flag store itself is allowed to exit the SB.
The Acquire Load as a One-Way Speculation Barrier (ARM64)
A CPU doesn’t wait politely for the ldar to retire before running the loads that follow it — it executes them speculatively, ahead of time. So how does acquire ordering survive speculation?
- The Guarantee (
ldar): Load-acquire is a one-way fence: no memory access after it may become visible before it. The acquire load itself simply reads whatever value is currently coherent for its address — the "freshness" of the published flag comes from the release/acquire pairing, not from the load re-querying anything. - The Mechanic (speculation + replay): The hardware lets later loads run early, but it keeps watching the coherence fabric. If another core invalidates a line that one of those speculative loads already read, the pipeline detects the hazard, squashes the offending loads, and replays them against fresh data. On Intel this event has a name — a memory-order machine clear (visible in the
machine_clears.memory_orderingperf counter); ARM cores recover analogously. - The Net Effect: Speculation buys you the latency win in the common case, and the squash-and-replay machinery makes it as if nothing after the acquire ever jumped ahead of it. The barrier is enforced by recovery, not by stalling.
3. The Local Illusion: Store Forwarding (Same-Address Mechanics)
When a single thread executes a store followed immediately by a load to the exact same memory address (e.g., mov [mem], eax followed by mov ebx, [mem]), the code works perfectly and instantly.
- The Interception: To avoid waiting for the SB to drain to the coherent system, a specialized hardware circuit scans the local SB for address matches.
- The Shortcut: The CPU intercepts the read request and copies the data straight out of the SB into arch register
EBX. - The Multi-Threaded Blind Spot: Store Forwarding creates a local illusion of time. The local thread sees its own write instantly, but because the data is still trapped inside the local SB, it remains entirely invisible to all other threads on the coherent system.
4. The Intel x86–64 Exception: Why release is Free but seq_cst Uses xchg
Intel x86–64 uses a strong hardware memory model called Total Store Ordering (TSO). This hardware design completely alters how atomic instructions compile.
Why release Costs Nothing on Intel
On x86–64, the hardware store buffer is strictly a First-In, First-Out (FIFO) queue for sequential stores.
- Because it is physically impossible for a later store to pass an earlier store inside Intel’s SB, Store-Store reordering cannot happen.
- Therefore, the compiler maps a C++
releasestore to a plain, standardmovinstruction. It costs zero extra hardware cycles.
The Store-Load Loophole (SB Bypass)
While Intel’s SB strictly enforces FIFO order for stores, it contains a loophole: it allows a subsequent load instruction to bypass pending writes sitting in the SB.
If a core loads an address not currently in its SB, it queries the coherent system immediately, ignoring its own un-flushed writes to other addresses. This creates a synchronization failure:
Core 1 Core 2
-------------------- --------------------
mov [X], 1 mov [Y], 1
mov eax, [Y] mov ebx, [X]
- Core 1 moves
1into its SB for addressX. It is trapped in the SB. - Core 2 moves
1into its SB for addressY. It is trapped in the SB. - Core 1 executes its load for
Y, misses its own SB, and queries the coherent system. Because Core 2's write toYhas not exited its SB yet, Core 1 reads0into arch registerEAX. - Core 2 executes its load for
X, misses its own SB, queries the coherent system, and reads0into arch registerEBX.
Both cores allowed a load instruction to bypass their own pending writes, breaking Sequential Consistency (seq_cst).
Why seq_cst Forces xchg on Intel
std::memory_order_seq_cst demands a Global Total Order. It strictly forbids a load instruction from pulling data into arch registers if there are stores still lingering in the SB.
To enforce this, the compiler emits the xchg instruction. On Intel silicon, xchg carries an implicit lock prefix:
- It places a physical freeze on the execution pipeline.
- It forces the CPU to stall and completely drain the SB out into the coherent system.
- Only after the SB is entirely empty does the hardware allow the subsequent load instruction to execute.
(Note: Compilers use xchg instead of mov + mfence because modern Intel/AMD chips optimize the single xchg instruction faster in hardware).
5. Architectural Comparison Matrix

6. High-Performance Spinlock Implementation
#if defined(__x86_64__) || defined(_M_X64)
#include <immintrin.h>
static inline void cpu_relax() { _mm_pause(); }
#elif defined(__aarch64__)
// `yield` is the nominal analog of x86 PAUSE, but on many cores it's a
// no-op. `isb` actually stalls the pipeline (a defensible backoff), and
// `wfe`/`sevl` is the real low-power wait. `yield` is the safe default.
static inline void cpu_relax() { asm volatile("yield" ::: "memory"); }
#endif
class Spinlock {
private:
std::atomic<bool> lock_state{false}; // false = unlocked
public:
void lock() {
for (;;) {
// Phase 1 — TEST: spin on a *relaxed load*. Read-only, so the
// line stays Shared and generates zero coherence traffic until
// the owner actually releases it.
while (lock_state.load(std::memory_order_relaxed)) {
cpu_relax();
}
// Phase 2 — TEST-AND-SET: only now attempt the RMW. acquire on
// success pairs with the release store in unlock().
bool expected = false;
if (lock_state.compare_exchange_weak(expected, true,
std::memory_order_acquire,
std::memory_order_relaxed)) {
return; // acquired
}
// Lost the race — fall back to the read-only spin.
}
}
void unlock() {
// Publishes the critical section before the flag drops.
lock_state.store(false, std::memory_order_release);
}
};
7. Architectural Takeaways
- Compiler Fences are Non-Negotiable: Even though Intel hardware handles
acquireandreleasefor free via standardmov, you must declare them in C++ to prevent the compiler optimizer from swapping your code lines during compilation and to guarantee cross-platform portability. - No Selective Tracking: The CPU cannot selectively track individual variables. A release barrier is a blunt instrument that flushes everything currently pending in that core’s SB buffer.
- Pad Against False Sharing: Coherence operates at cache-line granularity (64 bytes), and the core can’t track sub-line ownership. If two atomics that different threads hammer share a line, every write by one thread invalidates the other’s copy — “false sharing,” a silent throughput killer. Give each independently-contended variable its own line with
alignas(64). (The opposite move — co-locating fields on one line — only helps when a single thread always touches them together, i.e. locality. Don't confuse the two: shared-and-written wants separation, private-and-grouped wants packing.) **_weakvs_strong:** Always usecompare_exchange_weakinside loops. It maps 1:1 to ARM64's underlying Load-Exclusive/Store-Exclusive hardware architecture without forcing the compiler to generate an unnecessary, hidden nested loop.
메타데이터
- post_id
- 6f238e2fc82f
- slug
- c-memory-orders-on-the-metal-acquire-release-semantics-explained-6f238e2fc82f
- url
- https://medium.com/@kivancgunalp/c-memory-orders-on-the-metal-acquire-release-semantics-explained-6f238e2fc82f
- canonical_url
- https://medium.com/@kivancgunalp/c-memory-orders-on-the-metal-acquire-release-semantics-explained-6f238e2fc82f
- author_url
- https://medium.com/@kivancgunalp
- status
- ok
- fetched_at
- 2026-06-28 10:39:35