← Back to list

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

Kivanc G · 2026-06-19 12:55 · 0 claps · 6.4 min read
#cpp #computer-architecture #parallel-programming #concurrency #cache
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow LNG · Linguistics & Language 💻 · Programming 🏛️ · Architecture

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:

  1. Architectural Registers — the named, fast core-local storage (EAX, X0) where active computation happens.
  2. Store Buffer (SB) — the per-core FIFO that holds committed writes before they reach memory.
  3. The Coherence Fabric — the interconnect that keeps every core’s view of memory in sync. (My shorthand: “the coherent system.”)
  4. 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 (stlr on 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_ordering perf 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++ release store to a plain, standard mov instruction. 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]
  1. Core 1 moves 1 into its SB for address X. It is trapped in the SB.
  2. Core 2 moves 1 into its SB for address Y. It is trapped in the SB.
  3. Core 1 executes its load for Y, misses its own SB, and queries the coherent system. Because Core 2's write to Y has not exited its SB yet, Core 1 reads 0 into arch register EAX.
  4. Core 2 executes its load for X, misses its own SB, queries the coherent system, and reads 0 into arch register EBX.

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

  1. Compiler Fences are Non-Negotiable: Even though Intel hardware handles acquire and release for free via standard mov, you must declare them in C++ to prevent the compiler optimizer from swapping your code lines during compilation and to guarantee cross-platform portability.
  2. 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.
  3. 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.)
  4. **_weak vs _strong:** Always use compare_exchange_weak inside 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