Orderingmemory modelTSOweak orderingx86ARMlanguage model

Hardware Memory Models Are Not Language Memory Models

Your CPU has a memory model. Your language has a different one. The compiler stands between them, and code that "works on x86 and breaks on ARM" has almost always been written against the hardware model of the machine it was tested on.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
What is the difference between the memory model my CPU implements and the one my language specifies, and why does code that works on x86-64 break on AArch64?
What you wrote
There is one set of rules about how memory behaves, and if my code follows them it works everywhere.
What the hardware does
There are two sets of rules. The ISA defines what the hardware may reorder. The language defines what your program may assume. The compiler translates between them, and only the language contract is portable.
This is a §224 mandatory distinction and the single highest-value thing in the module. Reasoning against the hardware model produces code that is correct on the machine you tested and silently wrong on a different ISA — a defect class that survives review, CI and staging, then appears on ARM servers or mobile devices.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Strong and weak, and where the mainstream ISAs sit

ISA-SPECIFICArchitectural permissions as documented, not the behaviour of a particular chip. An implementation may be stricter than the architecture requires; relying on that is unsound because the next implementation need not be.

Architectures sit on a spectrum defined by which of the four reorderings from Why Your Loads and Stores Happen Out of Order they permit. x86-64 is at the strong end: essentially Total Store Order, permitting only Store→Load. AArch64 and POWER are substantially weaker, permitting all four, and require explicit ordering instructions for guarantees x86 gives away.

The practical consequence is asymmetric in an unhelpful direction. Code written and tested on the strong model runs on the weak one and breaks; code written for the weak model runs on the strong one and is merely slightly over-fenced. Since a great deal of software is developed on x86-64 laptops and deployed to ARM servers and phones, the failure direction is the common one.

It is worth being precise about what "weak" costs you, because it is not a matter of degree. On AArch64 a plain store to data followed by a plain store to ready can be observed in the opposite order by another core. There is no window, no race condition in the timing sense, no rare interleaving — the architecture simply does not order them, and no amount of retrying or testing on x86 will reveal it.

Where the mainstream architectures sit. "Permitted" means another core may observe the operations in the opposite order.
Propertyx86-64AArch64POWER
Store→Load reorderingPermittedPermittedPermitted
Store→Store reorderingForbiddenPermittedPermitted
Load→Load reorderingForbiddenPermittedPermitted
Load→Store reorderingForbiddenPermittedPermitted
Dedicated ordered load/store formsNot needed for most casesLDAR / STLRlwsync / sync idioms
Cost of a sloppy publish patternUsually appears to workFails in practiceFails in practice

The language model is the one you actually program against

C++11 and later, Java 5 and later, Go and Rust each define their own memory model, and those models are what your program is written against. They are deliberately *weaker* than any real hardware in some respects — permitting the compiler latitude — and *stronger* in others, most importantly by offering the DRF-SC guarantee described in Sequential Consistency: The Model You Already Have.

The compiler's job is to bridge the two. When you write a release store, the compiler knows both what your language promised and what the target architecture provides, and emits the minimum needed: often nothing at all on x86-64, an STLR on AArch64. This is why the same source file is correct on both, and why a hand-placed fence cannot compete — it encodes one target's requirements into portable-looking source.

The mandatory distinction, stated plainly: the hardware memory model is not the language memory model, and only the language one is portable. Knowing that x86-64 does not reorder stores tells you nothing about whether your C++ is correct, because the C++ model permits the *compiler* to reorder those stores regardless of what the CPU would have done. Reasoning at the hardware level skips the layer that is actually transforming your code.

One source line, three different notions of "what is guaranteed"
SOURCE (C++)
  flag.store(true, std::memory_order_release);

WHAT THE LANGUAGE MODEL PROMISES
  Everything sequenced before this store in this thread is
  visible to any thread that performs an acquire load which
  reads this value. Portable. Independent of any CPU.

WHAT THE COMPILER EMITS
  x86-64   mov BYTE PTR [rip+flag], 1     <-- no fence at all;
                                              TSO already forbids
                                              Store->Store reordering
  AArch64  stlrb w8, [x9]                 <-- dedicated release store

WHAT THE HARDWARE MODEL GUARANTEES
  x86-64   stores are not reordered with other stores
  AArch64  nothing, unless the ordered STLR form is used

NOTE THE TRAP: reading the x86 output and concluding "a release
store is just a mov, so a plain store is equivalent" is exactly
the reasoning that breaks on AArch64 -- and it also ignores that
the plain store leaves the COMPILER free to reorder it.

Why it breaks on ARM and not on your laptop

The canonical bug is the publish pattern with plain accesses. On x86-64 the two stores are not reordered by the hardware, so the only remaining risk is compiler reordering — which is real but intermittent, depending on optimisation level and inlining. Many such programs appear to work indefinitely.

Move the same binary's source to AArch64 and the hardware itself reorders the stores. The consumer observes ready == true with data still holding its old value, reliably enough to show up in ordinary testing — if anyone tests on that architecture. The bug did not appear when the code moved; it was always there, and the strong model was masking it.

The remedy is not to test more, though testing on weak hardware is worth doing. It is to stop reasoning about the hardware model at all in ordinary code. Use the language's synchronisation, keep the program data-race-free, and the DRF-SC guarantee makes the hardware model somebody else's problem — specifically, the compiler author's. The hardware model is worth understanding so you know *why* the rule exists, not so you can program against it directly.

Reasoning against the hardware model
1// "x86 doesn't reorder stores, so this is fine"
2config = load_config();
3config_ready = true; // plain store
4
5// Correct on x86-64 hardware. Still permits the
6// COMPILER to swap the two stores. And on AArch64
7// the hardware reorders them as well.
8// Two independent bugs, one of them invisible
9// on the machine it was written on.
Reasoning against the language model
1// "release/acquire gives me the edge I need"
2config = load_config();
3config_ready.store(true, release);
4
5// Constrains the compiler on every target.
6// Emits nothing extra on x86-64 (TSO already
7// guarantees it) and STLR on AArch64.
8// Correct everywhere, and free where it can be.

The two versions generate identical machine code on x86-64 in many cases. The difference is that one of them is *guaranteed* to be correct by the language, on every target, while the other happens to be correct on one architecture at one optimisation level. Identical output does not mean identical guarantees.

Key points

  • x86-64 permits only Store→Load reordering; AArch64 and POWER permit all four, so the strong model masks bugs the weak one exposes.
  • The language memory model, not the hardware one, is the contract your program is written against — and it is the only portable one.
  • The compiler bridges the two, emitting the minimum ordering each target needs, often nothing at all on x86-64.
  • Code that "works on x86 and breaks on ARM" was reasoning about the hardware model instead of the language model.
  • Identical machine code on one target does not mean identical guarantees across targets.

Progressive depth

Overview

Your CPU has rules about what it may reorder, and your language has its own rules about what your program may assume. They are not the same rules, and only the language's are portable.

Practical

Program against the language model. Use atomics and locks; keep the program data-race-free. Do not reason from "x86 does not reorder stores" — that fact is true and irrelevant, because the compiler reorders anyway and the next target does not share the guarantee.

Advanced

Learn the four-reordering table and where the mainstream ISAs sit. Read the assembly your atomics generate on both x86-64 and AArch64 — seeing a release store become a bare mov on one and STLR on the other is what makes the two-model distinction concrete rather than theoretical.

Internals

Formal models add multi-copy atomicity: whether a store becomes visible to all other observers simultaneously. x86-64 TSO and ARMv8 AArch64 are other-multi-copy-atomic; POWER historically was not, permitting two cores to disagree about the order of two stores from two other cores. Independent-reads-of-independent-writes is the litmus test that distinguishes these, and it is the point at which informal reasoning fails completely and tooling becomes mandatory.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Source → language model: an acquire/release pair states an ordering requirement independent of any CPU.
  2. 2
    Language model → compiler: the compiler must preserve that requirement and is otherwise free to reorder under the as-if rule.
  3. 3
    Compiler → target ISA: it emits the cheapest encoding that satisfies the requirement — no instruction on x86-64, STLR on AArch64.
  4. 4
    ISA → hardware: the architecture guarantees the ordering the emitted instructions request, and nothing more.
  5. 5
    Hardware → other cores: those cores observe an order consistent with what was requested, on both architectures, from the same source.
What people conclude from this — wrongly
  • "x86 is sequentially consistent" — it is not; it permits Store→Load, which is exactly what the store-buffer litmus test exposes.
  • "The atomic compiled to a plain store, so I can use a plain store" — the atomic also constrained the compiler, and on AArch64 it emits a different instruction entirely.
  • "ARM is buggy / has a weaker guarantee than it should" — it implements its documented model correctly; the code assumed a model it never promised.
  • "We will fix the memory model issues when we port" — they are already present; the port only makes them observable.

Consequences, controls and cost

What it causes
  • • Software developed on x86-64 and deployed to ARM carries a class of latent bugs that x86 testing cannot detect even in principle.
  • • Porting to a weakly-ordered architecture surfaces defects that look like new bugs but predate the port entirely.
  • • Reading x86 disassembly and concluding "the atomic compiled to a plain mov, so the atomic was unnecessary" produces code that is wrong on ARM and wrong under a more aggressive optimiser.
  • • Libraries tested only on one architecture cannot honestly claim portable thread safety.
What you can do
  • • Program against the language memory model exclusively; treat the hardware model as background knowledge explaining why the rules exist.
  • • Keep programs data-race-free so the DRF-SC guarantee applies and the hardware model becomes irrelevant to correctness.
  • • Run concurrency tests on at least one weakly-ordered architecture — an x86-only suite cannot exercise three of the four reorderings.
  • • When reviewing lock-free code, ask which memory model the argument is written against; if the answer is "x86", the review has found a bug.
How to see it
  • • Compile the same atomic operations for x86-64 and AArch64 and diff the generated instructions to see what each model requires.
  • • Run message-passing and store-buffer litmus tests on both architectures and compare observed outcome frequencies.
  • • Add an AArch64 target to CI for any code with concurrency, and run the suite there rather than only on x86-64.
  • • Use a thread sanitiser, which reasons about the language model rather than the hardware and so flags issues on any host.
What it costs
  • • Portable ordering costs instructions on weak targets that a hand-tuned x86-only implementation would omit.
  • • Testing on multiple architectures costs CI capacity and time, and is still evidence rather than proof.
  • • Relaxed atomics let you recover weak-target performance but shift the entire correctness burden onto reasoning that no tool checks well.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • ISA-SPECIFICThe reordering table is per-architecture: x86-64 TSO-like, AArch64 and POWER weaker. Implementations may exceed their architectural guarantees; relying on that is unsound.
  • PLATFORM-SPECIFICLanguage memory models are defined by C++11-and-later, Java 5-and-later, Go and Rust specifications, and differ from one another as well as from any hardware model.
  • SIMPLIFIEDReal models are formalised in far more detail — multi-copy atomicity, dependency-ordered-before, and the distinction between architectural permission and implementation behaviour are all omitted here.

Misconceptions

Claim
“There is one memory model and it belongs to the CPU.”
Reality
There are at least two that matter: the ISA's and the language's. Your program is written against the language's, and the compiler is what makes that true on each target.
Claim
“x86-64 is strongly ordered, so memory ordering does not matter there.”
Reality
It permits Store→Load reordering, and the compiler reorders regardless of the hardware. Both classes of bug occur on x86-64; it merely masks the other two hardware reorderings.
Claim
“If the generated assembly is identical, the two versions are equivalent.”
Reality
Identical output on one target at one optimisation level says nothing about another target, or about what the optimiser is permitted to do to the surrounding code.

Where the rest of this lives

Concurrency & Parallelism
Language memory models and data-race freedom

The DRF-SC bargain, what constitutes a data race, and which operations create synchronisation edges are language-model questions. This lesson covers only the hardware side and why the two must be kept apart.

Programming Languages & Runtime Internals
How a compiler lowers atomics per target

The mapping from a C++ or Java memory-order argument to concrete instructions is a code-generation concern, and it is where the two models are actually reconciled.