Dead Code Elimination
How unused computations and unreachable code are removed entirely.
Code That Does Nothing
Not every line you write contributes to the program's result. Sometimes a computed value is never used. Sometimes a branch can never be taken. This is dead code, and a good optimizer simply removes it. The result you observe is unchanged, but the instructions that produced unused or unreachable work are gone.
There are two flavours, and this lesson covers both:
- Unused results: a value is computed but never read. Eliminating it is sometimes called dead store elimination.
- Unreachable code: a branch the compiler can prove will never execute.
A Function With Both Kinds
Here is a function that contains some genuinely useful work surrounded by code that does nothing:
int compute(int x)
{
int unused{x * 99};
int result{x + 1};
if (false)
{
result = result * 1000;
}
return result;
}
Read it as the compiler must. unused is computed but never read by anyone; the function returns result, not unused. And the if (false) block can never run, so the * 1000 inside it is unreachable. The only line that actually affects the return value is result = x + 1.
At -O0, the compiler does not reason about any of this. It computes everything you wrote:
compute(int):
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov eax, DWORD PTR -20[rbp]
imul eax, eax, 99
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -20[rbp]
add eax, 1
mov DWORD PTR -8[rbp], eax
mov eax, DWORD PTR -8[rbp]
pop rbp
ret
There it is: imul eax, eax, 99 faithfully computes x * 99 and stores it into the unused slot at -4[rbp], even though nothing will ever read it. (The if (false) block produced no instructions even at -O0, because the compiler can drop a literally-constant-false branch trivially. The wasted multiply, however, is fully present.)
The Optimized Version
Now -O2:
compute(int):
lea eax, 1[rdi]
ret
Two instructions. lea eax, 1[rdi] computes x + 1 (the address-calculation form again, here just adding 1 to rdi) and leaves it in eax, ready to return. That is the whole function.
The x * 99 multiplication is completely gone. The optimizer traced the data flow, saw that unused is written and never read, and removed both the computation and the store. There was no point computing a number nobody would ever look at. The unreachable if (false) branch, naturally, contributes nothing either. What remains is exactly the work that influences the return value, and not one instruction more.
"But I Wrote That Code"
This is the part that surprises people the first time they see it: you wrote int unused{x * 99};, and in the optimized binary that multiplication does not exist. This is not the compiler ignoring your code or making a mistake. It is the compiler honouring the as-if rule: it may do anything it likes internally, as long as the program's observable behavior is as if it had executed your code exactly. Since the result of x * 99 is never observed, computing it or not makes no observable difference, so the optimizer is free to skip it.
This is also why dead code elimination is so tightly linked to the other optimizations in this chapter. Inlining and constant propagation constantly create dead code. When a constant gets folded, the variables that fed it become unused. When a function is inlined and its result is partly ignored, the ignored part becomes dead. Dead code elimination is the cleanup crew that sweeps away whatever the other passes leave behind.
A Caution About "Unused"
Be careful with the intuition that "the compiler removes anything unused". It removes work whose result is unobserved and whose computation has no side effects. A function call that prints to std::cout, writes to a file, or modifies a global has observable side effects, so even if you ignore its return value, the call stays. The optimizer only deletes work it can prove changes nothing you could detect. Pure arithmetic into a discarded variable is the easy case; anything that touches the outside world is not.
Reading the Clue
When a -O2 listing is dramatically shorter than your source suggests, and you have already accounted for folding and inlining, dead code elimination is usually the rest of the story. The practical reading skill is to ask, for each value your source computes, "does anything observable depend on this?" If the answer is no, expect it to be absent from the optimized listing. Lines that feed the return value, a side effect, or a later computation survive; lines that feed nothing do not.
Key Takeaways
- Dead code elimination removes computations whose results are never used and branches that can never execute.
- An unused result like
x * 99stored into a variable nobody reads is dropped entirely at-O2. - Unreachable code such as an
if (false)block produces no instructions. - This is justified by the as-if rule: the compiler must preserve observable behavior, and unobserved work makes no observable difference.
- Other optimizations (inlining, constant folding) constantly create dead code; elimination is the cleanup pass that removes it.
- Work with observable side effects (printing, I/O, modifying globals) is never removed just because its return value is ignored.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Dead Code Elimination - Quiz
Test your understanding of the lesson.
Practice Exercises
Revive the Dead Computation
Fix scaledSum so it returns the computed result instead of the unused fallback variable. Before you fix it, switch the assembly panel to -O2 and notice that the (a + b) * 3 computation is completely absent: because nothing observable depends on result, dead code elimination removed it. After the fix, the computation appears in the listing. Compare the two versions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!