Reading Assembly

Part 3 · Reading compiler output

`-O0` is readable, and a lie

Every tutorial teaches unoptimised output because it maps to the source. That mapping is exactly what makes it useless for understanding real code.

30 mincontrasting casesPOE
What this lesson is trying to break (2)
  • Unoptimised assembly shows what the code really does.
  • Optimisation is a small polish on top of the -O0 shape.

Almost every assembly tutorial you will find uses -O0 output. There is a good reason and it is also the reason you should be suspicious of it.

The contrast

int add3(int a, int b, int c) {
    int t = a + b;
    return t + c;
}

Compile it twice — once with the default -O2, then edit the flags to -O0 in Compiler Explorer and compile again.

Varies: only the optimisation flag — the source is byte-identical

-O0-O2
Instructions~10~3
Where do a, b, c live?Stack slots. Stored on entry, reloaded before each use.Registers, never written to memory
Does t exist?Yes — its own stack slotNo — folded away entirely
Frame pointerAlwaysOmitted
Maps to source lines?Almost one-to-oneLoosely at best
Good forDebugging, reading the ABIEverything you will actually ship

-O0 stores every argument to the stack and reads it back before use. That is not inefficiency by accident — it is the point: a debugger can then find any variable at any moment.

At -O0 the compiler makes a promise: every variable you wrote has a home in memory, and its value there is current at every statement boundary. That is what makes single-stepping and inspecting variables work reliably. Paying for it means a store and a reload around every use.

At -O2 that promise is dropped. Variables live in registers, or in several registers at different times, or nowhere at all. This is why debugging optimised code shows you <optimized out>: the variable genuinely has no location.

Predict firstcommit in writing, then look

For add3 above, count the loads and stores in each version.

Why teaching with -O0 backfires

The pedagogical argument for -O0 is obvious: each source line becomes a small group of instructions, so you can see the correspondence and build confidence.

The problem is that the correspondence is the thing real code does not have. Learn to read -O0 and you learn to expect:

  • one storage location per variable → wrong, they live in registers and move
  • source order preserved → wrong, instructions are reordered freely
  • one function per function → wrong, small ones are inlined away
  • loops that look like loops → wrong, they are unrolled, vectorised, and their counters replaced by pointer walks

Every one of those expectations has to be unlearned before you can read -O2, which is where all the code you care about lives. So this course started at -O2 and will stay there.

When -O0 is the right tool

Not never — just not by default.

To see the ABI plainly. Arguments are visibly stored, locals visibly allocated. If you want to understand 2.2 concretely, -O0 shows the machinery instead of optimising it away.

To see what the source literally asked for. Occasionally you want to confirm that a construct means what you think before the optimiser has an opinion — useful when investigating whether an optimisation is legal.

To debug. Reliable variable inspection is worth the slowdown, which is why debug builds exist.

There is also a middle option worth knowing: -Og optimises while trying to preserve debuggability. It is the sensible default for a debug build that still has to run at a tolerable speed.

Explain it to yourselfwrite it — thinking it does not count

A tutorial you are reading shows a loop at -O0 and concludes that “each iteration costs about 8 instructions, so 1000 iterations costs 8000”.

Write down every assumption in that sentence that fails at -O2. There are at least three, and one of them is not about optimisation at all — it is fiction 2 from 0.2.

What you should now be able to say

  • -O0 gives every variable a stack slot and reloads it before every use, to keep a debugging guarantee.
  • Its readability comes from a source correspondence that real code lacks, so the reading skill does not transfer.
  • At -O2 variables live in registers or nowhere — hence <optimized out>.
  • -O0 is the right tool for seeing the ABI, checking what was literally requested, and debugging; -Og is the compromise.

Next: 3.3 — The optimisations you will actually see.