A compiler sees: `x = a * b + c` on line 5. Variable x is overwritten on line 12 without being read between lines 5 and 12. Variables a, b, and c are each used only in this expression. What does dead code elimination do?
AIt removes only line 5, since x is dead after that assignment, but a, b, and c remain because they were 'used' on line 5
BIt removes line 5 and also removes any assignments that produced a, b, and c, since those values are now also dead
CIt cannot remove line 5 because a, b, and c might have side effects from their definitions
DIt marks line 5 as unreachable code and uses control flow analysis to eliminate it
Dead code elimination cascades. Removing the dead assignment `x = a * b + c` makes the prior computations of a, b, and c dead as well — if their only use was on line 5, they are no longer live anywhere. A second pass (or aggressive DCE in a single pass) removes those assignments too. This cascading effect is why compilers run DCE iteratively or use the aggressive approach that starts by assuming everything dead and marks live only what contributes to observable effects.
Question 2 Multiple Choice
What is the fundamental difference between dead assignment elimination and unreachable code elimination?
ADead assignment elimination is safe; unreachable code elimination might change program behavior
BDead assignment elimination relies on live variable analysis to identify values that are computed but never read; unreachable code elimination relies on control flow analysis to find basic blocks with no reachable predecessors
CDead assignment elimination only works on scalars; unreachable code elimination works on any code block
DUnreachable code elimination is a subset of dead assignment elimination — all unreachable code contains dead assignments
The two are distinct optimizations that use different analyses. Dead assignments exist in reachable code — the code runs, but the computed value is never subsequently read. This requires live variable analysis: is this variable live after this definition? Unreachable code cannot execute at all — it lies on no path from the entry block in the control flow graph. This requires control flow analysis, not liveness. Both are called 'dead code' colloquially, but a compiler handles them through different mechanisms.
Question 3 True / False
Dead code elimination can reveal additional dead code, because removing a dead assignment may make the computations that produced the assigned value dead as well.
TTrue
FFalse
Answer: True
True — this cascading property is one of the defining characteristics of DCE and why compilers run it iteratively or use the aggressive (mark-from-observable-effects) approach. If x is dead, its definition `x = a * b + c` is removed. If a, b, and c were defined only to be used in that expression, their definitions are now dead. Removing those may make further upstream computations dead. The chain can propagate arbitrarily deep through the program's def-use graph.
Question 4 True / False
If a code block is seldom executed during any test run, a compiler can determine it is unreachable and safely eliminate it.
TTrue
FFalse
Answer: False
False. Unreachable code is a *static* (compile-time) property determined by control flow graph analysis — a block is unreachable if it has no predecessors other than itself in the CFG. Test execution is a *dynamic* property; code that was never executed during testing might be reachable under different inputs. A compiler cannot use test coverage data to decide what to eliminate — it would change the program's semantics for those untested inputs. Only static analysis proves unreachability.
Question 5 Short Answer
What makes 'aggressive' dead code elimination different from the naive approach, and why is it particularly effective after function inlining?
Think about your answer, then reveal below.
Model answer: The naive approach finds statements that are dead (value never read) and removes them. Aggressive DCE works in the opposite direction: it starts by assuming everything is dead, then marks statements as live only if they contribute to observable effects (return values, memory writes visible outside the function, I/O). Everything not marked live is deleted in a single pass, naturally handling chains of dead code. After inlining, large sections of the inlined function's code may be irrelevant in the caller's context — variables never read, branches never taken — and aggressive DCE eliminates all of it in one pass without needing to iterate.
The key conceptual difference is the starting assumption. Naive DCE asks 'is this specific statement dead?' and must iterate to catch cascades. Aggressive DCE asks 'does this statement contribute to any observable program effect?' and propagates liveness backward from observable effects, naturally handling arbitrarily long chains of dead code in one traversal. Inlining amplifies this benefit because it often creates large regions of code that are locally correct but globally irrelevant to the caller's purpose.