Comparing -O0 and -O2
The same function at both optimization levels, and what the optimizer strips away.
Two Listings for the Same Code
Every example so far in this course has shown you assembly at one optimization level or another, usually noting in passing that -O2 would look different. This chapter is where we finally put the two side by side and study the gap between them. That gap is the optimizer's work, and learning to read it is one of the most useful things you can do with assembly.
The compiler has optimization levels, selected with a flag:
-O0("oh-zero") means no optimization. The compiler translates your code in the most literal, step-by-step way it can. This is the default for debug builds.-O2("oh-two") turns on a large set of optimizations. This is what a typical release build uses.
The C++ source is identical at both levels. What changes is how much plumbing the compiler is willing to remove.
A Function Worth Comparing
Here is a small function that does a little arithmetic with two arguments:
int sumRange(int a, int b)
{
int total{a + b};
int doubled{total * 2};
return doubled - a;
}
Three locals, three operations. Nothing clever. Let us compile it both ways.
The Unoptimized Version
At -O0:
sumRange(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov DWORD PTR -24[rbp], esi
mov edx, DWORD PTR -20[rbp]
mov eax, DWORD PTR -24[rbp]
add eax, edx
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
add eax, eax
mov DWORD PTR -8[rbp], eax
mov eax, DWORD PTR -8[rbp]
sub eax, DWORD PTR -20[rbp]
pop rbp
ret
Fourteen instructions. Read it slowly and a pattern jumps out: almost every line touches the stack. The arguments arrive in edi and esi and are immediately stored into stack slots -20[rbp] and -24[rbp]. Each local (total, doubled) gets its own slot too, at -4[rbp] and -8[rbp]. Every time the code needs a value it has just computed, it stores it to the stack and then loads it straight back.
Look at this trio in the middle:
add eax, edx ; compute total = a + b
mov DWORD PTR -4[rbp], eax ; store total to its stack slot
mov eax, DWORD PTR -4[rbp] ; ... and immediately load it back
The store and the very next load are completely redundant: the value is already in eax. But at -O0 the compiler does not try to notice this. It treats every named variable as a real location in memory and dutifully writes it out and reads it back. That literalness is exactly what makes -O0 easy to step through in a debugger, and exactly what makes it slow.
The Optimized Version
Now the same function at -O2:
sumRange(int, int):
lea eax, [rdi+rsi]
add eax, eax
sub eax, edi
ret
Four instructions, and not a single one touches the stack. There is no prologue (push rbp / mov rbp, rsp) and no epilogue (pop rbp) because the function never needs a stack frame: every value lives in a register from start to finish.
Walk through it:
lea eax, [rdi+rsi]computesa + bin one step. (leais normally an address calculator, but it doubles as a cheap way to add registers; you saw this trick in Chapter 2.) That istotal.add eax, eaxdoubles it. That isdoubled.sub eax, edisubtractsa. That is the return value, already sitting ineax.retreturns.
The three locals never became memory at all. The optimizer kept them in registers, fused the work, and dropped the bookkeeping. Same result, a quarter of the instructions.
What the Optimizer Removed
Lining the two up, the differences fall into a few clear categories:
At -O0 |
At -O2 |
|---|---|
Full stack frame (push rbp / mov rbp, rsp / pop rbp) |
No stack frame at all |
| Each argument and local stored to its own stack slot | Values kept in registers |
| Store-then-immediately-reload of every intermediate value | No redundant memory traffic |
| 14 instructions | 4 instructions |
None of this changed what the function computes. The optimizer is not allowed to change observable behavior; it is only allowed to reach the same answer with less work. Everything it removed here was pure overhead: memory traffic that existed only because -O0 translates each statement in isolation.
Why Both Levels Exist
If -O2 is so much leaner, why does -O0 exist at all? Two reasons.
First, debugging. At -O0, every variable has a real home in memory and every source line maps cleanly to a block of instructions, so a debugger can show you exactly what is happening. At -O2, variables live in registers that get reused, lines are reordered and fused, and whole computations vanish, which makes single-stepping confusing.
Second, build speed. Optimization takes time, so -O0 compiles faster, which matters during rapid development.
For reading assembly, the two levels teach different things. -O0 is where you learn the literal shape of a construct: the prologue, the stack slots, the store/reload rhythm. -O2 is where you learn what your code actually costs once the overhead is stripped away. Throughout this chapter we will keep showing both, because the contrast is where the insight lives.
Try It Yourself
In this lesson's exercise you will write a short function, run it, and then use the assembly panel to flip between -O0 and -O2. Counting the instructions yourself, and watching the stack traffic disappear, makes everything above concrete.
Key Takeaways
-O0means no optimization: literal, step-by-step code with a full stack frame.-O2enables a broad set of optimizations.- The C++ source is identical at both levels; only how much overhead the compiler removes changes.
- At
-O0, arguments and locals get their own stack slots, with constant store-then-reload of intermediate values. - At
-O2, values stay in registers, the redundant memory traffic disappears, and short functions often need no stack frame at all. - The optimizer never changes the result; it only removes overhead to reach the same answer with fewer instructions.
-O0exists for easier debugging and faster builds;-O2is the typical release setting.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Comparing -O0 and -O2 - Quiz
Test your understanding of the lesson.
Practice Exercises
Watch the Stack Traffic Disappear
Complete weightedScore so it computes the score in three steps using the local variables base, total, and adjusted. The program prints three results. Once the output is correct, read the assembly panel and switch between -O0 and -O2: at -O0 you will see each local stored to and reloaded from the stack, while at -O2 the whole function collapses into a handful of register-only instructions with no stack frame.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!