Two layers, and why this guide is the floor
Everything in guides 1 through 4 lived one level up, in the language's abstract memory model: sequential consistency as the ideal, acquire and release as the everyday tools, fences and read-modify-writes as the sharper instruments. That model is deliberately written without naming any chip, so that the same C or C++ source has the same defined meaning on a desktop x86, a phone's ARM core, or a chip nobody has built yet. This final guide goes the other way: down to the metal, to the actual rules two real processor families enforce. Understanding the floor is what turns the abstract model from a set of memorized words into something you can feel.
Here is the punchline up front, so the details have a home: x86 is strongly ordered and ARM is weakly ordered. On x86 the hardware already keeps almost all of your reads and writes in order on its own, so an acquire or a release usually costs zero extra instructions. On ARM the hardware reorders freely, so the same acquire or release must emit a real barrier or a special ordered load/store. The reason your code does not care — the reason you write one memory_order and ship to both — is that the compiler quietly fills exactly the gap each chip leaves. The portable model is the promise; this guide is how the promise is paid for on each machine.
x86 TSO: strong, but with one famous hole
The x86 model has a name: TSO, for Total Store Order, and x86-TSO is worth pinning down precisely because it is so close to the single-notebook intuition that you can almost trust it — almost. TSO guarantees that all stores from all cores become visible in one global order that every core agrees on, and that each core sees its own and others' stores in program order, with exactly one exception. The exception is the store buffer you met in guide 1: a core's own store can sit in its private buffer and be delayed past a later load to a different address. So on x86 the only reordering the hardware allows is store-then-load, where the store and load touch different locations. Load-load, load-store, and store-store are all kept in order for you, free.
That one hole is not a curiosity — it is exactly the store-buffering outcome from guide 1, where two threads each write their own variable then read the other's, and both can read the stale 0. Now you can see the mechanism is not the compiler at all here: even with the instructions in perfect program order, TSO permits each store to linger in its buffer while the following load to a different address goes ahead. This is why the store-buffering litmus test is the one classic that fails even on the famously strong x86. Everything else your intuition expects — that once a value reaches memory all cores see it consistently — TSO does deliver, which is why so much sloppy lock-free code accidentally works on x86 and then shatters when ported to a phone.
ARM weak: nearly everything can move
ARM (and POWER, and RISC-V's relaxed mode) sit at the other end. A weakly ordered model keeps far less for you: by default, two memory accesses to different addresses can be reordered in any of the four directions — load-load, load-store, store-load, store-store — so long as a single thread, looking only at its own values, cannot tell. The hardware does this for the same reason the compiler does: speed. An out-of-order core has many instructions in flight, a load that misses cache can be overtaken by a later load that hits, and stores drain from buffers whenever the memory system is ready, not in any tidy program order visible to other cores. On x86 most of that is hidden from other cores; on ARM it is exposed, and your code must pay to hide what it needs hidden.
So the same release store that compiled to a plain mov on x86 must, on ARM, become a barrier-bearing instruction — historically a dmb (data memory barrier) before or after the access, and on modern ARMv8 a dedicated one-way ordered instruction: stlr for a release store and ldar for an acquire load, which carry the half-fence semantics in the instruction itself. This is the concrete payoff of guide 3's one-way-door image: on weak hardware the door is a real machine instruction that genuinely stops the chip from floating accesses across it. The lesson that bites people hardest is that code which races but happens to work on x86 is not 'mostly correct' — it is undefined under the memory_order rules, and ARM is simply the machine that finally collects the debt.
Same source: flag.store(1, memory_order_release);
x86-64 (TSO, strong):
mov DWORD PTR [flag], 1 ; an ordinary store; no barrier needed
AArch64 (ARM, weak):
mov w8, 1
stlr w8, [flag] ; store-release: carries the ordering
Same source: v = flag.load(memory_order_acquire);
x86-64: mov eax, DWORD PTR [flag] ; ordinary load
AArch64: ldar w0, [flag] ; load-acquire: carries the orderingTwo costs that only appear on weak hardware
Guide 4 said compare_exchange_weak() is allowed to fail spuriously, and promised this guide would show why. Here it is, concrete. ARM has no single atomic compare-and-swap primitive in its classic form; instead it offers a pair, load-linked / store-conditional. The load-linked (ldxr) reads a value and quietly tags that cache line as 'reserved'; the store-conditional (stxr) writes back only if the reservation is still intact, and reports success or failure. Any intervening event on that line — another core touching it, sometimes even an unrelated interrupt or context switch — clears the reservation, so the store-conditional can fail for reasons that have nothing to do with the value. That is precisely the spurious failure, and it is why the weak form, which simply loops on it, generates tighter code than the strong form, which must hide an inner retry to mask the spurious miss.
The second weak-hardware cost is subtler and lives in your data layout, not your ordering: false sharing. The unit of coherence is not a byte but a cache line, typically 64 bytes. If two threads hammer two different atomics that happen to sit in the same 64-byte line, the MESI protocol bounces that line back and forth between the cores' caches as if they were sharing a single variable, even though logically they share nothing. The fix is to pad hot per-thread atomics onto separate lines (C++ even names the magic number, std::hardware_destructive_interference_size). This is not a memory-ordering bug — your program is still correct — but it can erase the entire speed advantage that pushed you toward atomics in the first place, and it is far more visible on the many-core machines where weak ordering also lives.
Putting the whole rung together
Step back and the shape of the rung is clear. The language gives you an abstract contract; two real chips honor it with very different machinery; and the compiler is the translator that makes one source program correct on both. The danger is doing your testing only on x86 and your reasoning only on the single-notebook intuition — because x86's strength hides the bugs that ARM will expose. The discipline that protects you is to write to the model, never to a chip: pick the memory_order that expresses the ordering you actually need, and trust the toolchain to spend exactly the right number of barriers on each target.
- Know which model is which: x86 is TSO (strong) — only store-then-load to different addresses can reorder; ARM is weak — all four directions can reorder. POWER is weak too; RISC-V can be configured either way.
- Reason about correctness using the language's model and the data-race-free contract, never using 'it worked on my x86 laptop' — the strong chip is the worst place to discover an ordering bug.
- Expect the same source to compile differently: zero extra instructions for acquire/release on x86, real ldar/stlr or dmb barriers on ARM. That difference is the model being paid for, not a portability bug.
- Test on weak hardware (a real ARM machine) and with the thread sanitizer; together they catch the races and missing happens-before edges that x86 silently forgives.