Connectionscompileroptimizationreorderingas-iftoolchainsemantics

The Compiler Reordered It Before the CPU Did

Between the line you wrote and the work the machine performs sit two independent reordering layers: a compiler that transforms code under the language's rules, and a processor that executes the result out of order under the architecture's rules. Each preserves its own notion of observable behaviour, and neither preserves the order you wrote.

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 actually happens to my source code between writing it and the CPU executing it, and who is allowed to reorder what?
What you wrote
The compiler translates my statements into instructions, roughly one construct at a time, and the CPU runs those instructions in order.
What the hardware does
The compiler is free to delete, merge, hoist, sink, vectorize and reorder anything as long as the program's *observable* behaviour under the language's abstract machine is preserved. The CPU then reorders the surviving instructions again, under a different and weaker set of rules.
Almost every confusing performance result and every "it works on my machine but not on ARM" concurrency bug traces back to assuming one of these layers does not exist. Reasoning about cost or about inter-thread visibility from source order alone is reasoning about a machine nobody built.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Two layers, two different rulebooks

The compiler operates under an *as-if* rule: it may perform any transformation provided the program behaves as if it had executed according to the language's abstract machine. Crucially, "behaves as if" is defined in terms of a specific list of observable effects — typically I/O, volatile accesses and synchronisation — and *not* in terms of which instructions exist or what order they appear in. Everything not on that list is fair game.

The processor then operates under the architecture's memory model, which is a different and generally weaker contract. It guarantees that a *single* thread observes its own operations as if in program order, and makes much narrower promises about what other threads see and when (Hardware Memory Models Are Not Language Memory Models). Within a thread it will happily execute instructions as their operands become ready rather than in sequence (Out-of-Order Execution).

These layers are independent. A barrier that constrains the CPU does not constrain the compiler, and a compiler barrier does not constrain the CPU. This is precisely why language-level atomics exist: a memory_order annotation has to emit both the right machine instruction *and* prevent the compiler from moving things across it. Getting one and not the other produces code that works under -O0 and breaks under -O2, or works on x86-64 and breaks on AArch64.

what "correct" meansconstrainsemitsfetchedretires in ordermay delete work entirelySource codeLanguage semantics (abstract machine)Compiler optimizer — reorders under as-ifMachine instructions (ISA)CPU — reorders under the memory modelObserved behavior
UserLLMAgentToolDataDecisionHumanGuardrail

What the compiler is allowed to do to your loop

The list is longer than most people expect. It may hoist a loop-invariant computation out entirely, keep a variable in a register and never write it to memory, unroll the loop, vectorize it (Auto-Vectorization: Verify, Do Not Assume), fuse two loops into one or split one into two, replace a multiply with shifts, evaluate a constant expression at compile time — and delete code whose result is never observed, which is the transformation that silently destroys naive microbenchmarks (Every Way a CPU Microbenchmark Lies).

What it may *not* do is change observable behaviour as the language defines it. The subtlety is that the language's definition is narrower than intuition: the time a loop takes is not observable, so a loop with no observable effect may be removed. Reading an uninitialised value or overflowing a signed integer may be undefined, in which case the compiler is entitled to assume it never happens and optimise on that basis — which is how a bounds check can legally vanish.

The practical consequence is that source-level operation counting predicts neither the instruction count nor the cost. The only reliable way to know what the machine will run is to look at what the compiler emitted, which is a routine and underused activity: modern toolchains make the generated assembly easy to inspect, and reading it settles arguments that speculation cannot.

One source loop, several legal compilations — all with identical observable behaviour
1// As written
2sum = 0
3for i in 0..n:
4 sum += a[i] * 2
5
6// Legal: strength reduction, invariant hoisting, unrolling
7sum = 0
8for i in 0..n step 4:
9 sum += (a[i] << 1) + (a[i+1] << 1)
10 + (a[i+2] << 1) + (a[i+3] << 1)
11
12// Legal: vectorized - four lanes per instruction
13vsum = [0,0,0,0]
14for i in 0..n step 4:
15 vsum += load4(a + i) << 1
16sum = horizontal_add(vsum)
17
18// Legal if `sum` is never observed afterwards:
19// (the entire loop is removed)

Where this bites in practice

The three recurring failures are worth naming. Microbenchmarks that measure nothing: the timed loop computes a value the program never uses, the compiler deletes it, and the benchmark reports an impossibly fast result. Concurrency bugs that appear only under optimisation: a flag polled in a loop without an atomic or volatile annotation gets hoisted into a register, and the thread never sees the update. Portability failures: code that relies on x86-64's relatively strong ordering breaks on AArch64, which permits more reordering — the bug was always there, and the stronger hardware was hiding it (Why Your Loads and Stores Happen Out of Order).

The unifying diagnosis is that in each case the programmer had a contract in mind that the language did not actually provide. The fix is never to guess harder; it is to state the requirement in terms the compiler understands — an atomic with the ordering you need, a volatile for a memory-mapped register, a compiler barrier, or consuming the benchmark result so it cannot be deleted.

None of this is an argument against optimisation, and it is emphatically not an argument for writing assembly. The compiler is better at instruction selection and scheduling than almost anyone, and its transformations are why high-level code performs at all. The argument is only that it is a *transformation layer with rules*, and knowing the rules is what lets you predict its output and debug its results.

Which layer reordered it, and what actually constrains each
SymptomLayer responsibleWhat constrains itWhat does not
Benchmark loop runs impossibly fastCompiler removed dead codeConsuming the result; a compiler-barrier-style sinkA CPU memory barrier
Polled flag never observed by a threadCompiler hoisted the load into a registerAtomic or volatile access with suitable orderingAdding a sleep, which usually just hides it
Works on x86-64, fails on AArch64CPU reordered stores or loadsLanguage atomics emitting the right barriersCompiler-only barriers
Debug build correct, release build wrongCompiler optimisation exposed undefined behaviourFixing the undefined behaviour itselfLowering the optimisation level, which only hides it
Profiler blames a line that looks trivialCompiler merged or moved surrounding workReading the emitted assembly to see what mergedTrusting the source-to-line mapping

Key points

  • Two independent layers reorder your program: the compiler under the language's as-if rule, then the CPU under the architecture's memory model.
  • The as-if rule preserves a narrow list of observable effects; execution time and instruction order are not on it.
  • A compiler barrier does not constrain the CPU and a CPU barrier does not constrain the compiler — language atomics exist to address both at once.
  • Source-level reasoning cannot predict instruction count or cost; reading the emitted assembly can, and is easier than most engineers assume.
  • Bugs that appear only at higher optimisation levels or only on weaker-ordered hardware are almost always a missing contract, not a compiler bug.

Follow the mechanism

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

  1. 1
    Source → abstract machine: the language defines which effects are observable, and everything else becomes negotiable.
  2. 2
    Abstract machine → optimizer: the compiler applies transformations that preserve those effects, freely reordering, merging and deleting the rest.
  3. 3
    Optimizer → machine instructions: what is emitted may bear little resemblance to the source structure, having been unrolled, vectorized or elided.
  4. 4
    Machine instructions → CPU front end: the processor fetches them in order and then schedules execution by operand readiness, not by position (Out-of-Order Execution).
  5. 5
    CPU → observed behavior: results retire in program order within a thread, while other threads may observe a weaker ordering (Hardware Memory Models Are Not Language Memory Models).
What people conclude from this — wrongly
  • "The compiler has a bug" — overwhelmingly it is undefined behaviour in the source being exploited exactly as the standard permits.
  • "Lowering the optimisation level fixed it" — that hides the defect; the contract is still missing and will resurface.
  • "volatile makes it thread-safe" — volatile constrains the compiler, not the CPU, and provides no inter-thread ordering guarantees in most languages.
  • "The assembly will look like my source" — after inlining, unrolling and vectorization the correspondence is frequently unrecognisable.

Consequences, controls and cost

What it causes
  • • Timing a loop whose result is unused measures the empty loop the compiler left behind, or nothing at all.
  • • Concurrency code without explicit atomics can work in debug builds and fail in release builds, on the same machine.
  • • Code tested only on strongly-ordered hardware can carry latent ordering bugs that surface on a different architecture.
  • • Profiler attribution to specific source lines is approximate, because the instructions at that address may have come from several lines.
What you can do
  • • State ordering requirements explicitly with language atomics rather than relying on incidental hardware behaviour.
  • • Read the emitted assembly when performance or codegen is in question — it is the ground truth and it is one command away.
  • • Prevent dead-code elimination in benchmarks by consuming results in a way the compiler cannot see through.
  • • Test concurrency on weakly-ordered hardware, or under a tool that models weak ordering, rather than trusting a strong-ordered machine.
  • • Treat "only breaks with optimisations on" as a signal of undefined behaviour in your code, not of a compiler defect.
How to see it
  • • Inspect the generated assembly for the function in question and confirm the instructions you expected actually exist.
  • • Compare instruction counts across optimisation levels; a loop that vanished shows up as a step change.
  • • Use a thread sanitizer or a weak-memory model checker to expose ordering assumptions that a strong machine masks.
  • • Diff generated code before and after a source change to confirm the change reached the machine at all.
What it costs
  • • Reading assembly is a real skill with a real learning cost, and it is architecture-specific.
  • • Constraining the compiler with barriers and atomics gives up optimisation opportunities and can measurably slow hot paths.
  • • Writing code defensively against aggressive optimisation can make it less idiomatic and harder for the next reader.
  • • Testing on multiple architectures costs infrastructure that small projects often cannot justify.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALThe two-layer reordering model applies to every optimising compiler and every out-of-order processor, though the precise permissions differ by language and by architecture.
  • ISA-SPECIFICHow much the CPU may reorder is defined by the architecture: x86-64 is comparatively strong, while AArch64 and POWER permit substantially more, so identical source can behave differently across them.
  • SIMPLIFIEDThe pseudocode transformations are illustrative of legal optimisations rather than a description of any particular compiler's pass ordering or output.

Misconceptions

Claim
“The compiler translates my code; the CPU executes the translation in order.”
Reality
Both layers reorder. The compiler may delete, merge, hoist and vectorize under the as-if rule, and the CPU then executes what remains out of order by operand readiness. Neither preserves your written sequence, and each is governed by a different rulebook.
Claim
“Marking a variable volatile makes concurrent access safe.”
Reality
In most languages volatile constrains the *compiler* — it forces the access to happen and prevents caching in a register — but provides no guarantee about CPU-level ordering or atomicity. Inter-thread correctness needs language atomics, which emit the appropriate hardware barriers as well.
Claim
“If it only breaks with optimisations enabled, the optimiser is wrong.”
Reality
Almost always the source contains undefined behaviour that the optimiser is entitled to assume cannot occur. The unoptimised build merely happened to produce the behaviour you expected. The defect is in the code, and lowering the optimisation level conceals rather than fixes it.

Apply it

Where the rest of this lives

Programming Languages & Runtime Internals
Optimization passes, IR and the as-if rule

How a compiler represents your program internally, which passes run in what order, and how each language defines its abstract machine and its observable effects — this domain assumes the transformation happens and reasons about its consequences for the hardware.

Concurrency & Parallelism
Language memory models and happens-before

Which orderings a language guarantees between threads, and how to reason about interleavings — here we cover only the hardware's contribution to the reordering, not the correctness argument built on top of it.