Home · ASPLOS 2022 · Compilers & Programming Languages

Finding missed optimizations through the lens of dead code elimination

Theodoros Theodoridis, Manuel Rigger and Zhendong Su

Conference
ASPLOS 2022
Published
2022
Pages
697-709
Citations
45

Correctness bugs in compilers have a crisp test — wrong output — but missed optimizations had no oracle at all, so this paper builds one from dead code elimination by planting un-analyzable marker calls in every basic block; any marker one compiler deletes but another keeps is a pinpointed, reportable performance bug.

The argument in one glance

  1. Problem

    Performance bugs are invisible bugs

    A missed optimization — the compiler emits correct but needlessly slow code — has no failing test. It arises from forgotten corner cases in analyses, unintended interactions between passes, or a change that improves one workload while quietly degrading another. Decades of optimizer engineering ship with an unknown number of these.

  2. What people did

    Benchmarks, feature diffs, and solver-verified analyses

    Compiler teams track benchmark suites to spot regressions; research compared static code features (instruction counts, register spills) across compilers to flag suspicious output; SMT-solver work verified and sharpened specific data-flow analyses.

  3. Why it fell short

    No general oracle, no localization

    Benchmarks only catch regressions on the few programs measured and say nothing about where the miss lives. Feature comparisons flag symptoms without pinpointing causes. Solver-based verification targets one analysis at a time and needs deep compiler surgery. The field lacked what correctness testing has enjoyed for years: an automatic, general, black-box oracle.

  4. This paper's idea

    Use dead code elimination as the oracle

    DCE is an optimization "sink": whether a block can be proven dead depends on alias analysis, constant propagation, inlining, range analysis — much of the pipeline. So plant marker calls to undefined functions in every basic block; a marker vanishes from the assembly iff the compiler proved its block dead. Compare markers across compilers, optimization levels, and versions: every disagreement is a localized missed optimization. Result: 2,637 misses found in GCC and 318 in LLVM, 84 bug reports (62 confirmed or fixed), and 82 bisected regressions.

Why this problem is worth solving

Compiler correctness testing is a mature field with a devastating weapon: differential testing of program outputs. Generate a random program, run it under two compilers, and any output disagreement proves a miscompilation — automatically, at scale. (This site’s NNSmith explainer shows the same recipe applied to deep-learning compilers.) Compiler performance testing had no equivalent. A

a case where the compiler produces correct but inefficient code that it could reasonably be expected to improve — a performance bug rather than a correctness bug
produces the right answer, so no output comparison can catch it; and “the code could be faster” is not a mechanically checkable statement in general.

Yet the bugs are real and consequential. They come from forgotten corner cases, from pass-ordering interactions (enabling more inlining can starve a later analysis), and from silent regressions — this paper finds 82 cases where older GCC or LLVM versions optimized code that current versions no longer can. Every such miss taxes every program compiled thereafter, invisibly.

What makes the problem approachable is choosing the right observable. The paper’s insight is that

DCE — the transformation that removes code proven unreachable or unused. Crucially, proving code dead usually requires OTHER analyses to succeed first: aliasing facts, propagated constants, inlined callees, value ranges
functions as a summary statistic for the whole optimization pipeline. If the pipeline understood the program, the dead block disappears; if any prerequisite analysis fell short, the block — observable in the binary — survives. Optimization quality becomes a bit you can grep for.

What people did before, and where it broke

Benchmarking. The industry standard: run SPEC-class suites, watch for slowdowns. It detects regressions on those programs — not pre-existing misses, not programs outside the suite, and never with automatic localization to a culprit analysis.

Static-feature differential testing. Barany compared features of generated code (loads, spills) across compilers to flag potential misses — a symptom detector: a flagged file still requires manual archaeology to find what, where, and why.

Solver-backed analysis verification. Taneja et al. verified specific data-flow analyses against an SMT solver, finding real sharpness gaps — but one analysis at a time, with compiler modifications, by construction blind to interactions between passes.

The gap. Correctness testing thrives because its oracle is general, automatic, and black-box. Missed-optimization detection needed the same three properties, plus one more correctness testing gets for free: pointing at the location of the failure.

The DCE-lens pipeline

  1. Generate deterministic programs

    Csmith produces closed C programs — no inputs, no external state — so each program’s dead and alive blocks are fixed properties. The corpus: 10,000 files, 3.1 million instrumented basic blocks (89.6% dead, as expected for random code — convenient for stress-testing DCE).

  2. Instrument with optimization markers new

    Insert a call to a distinct UNDEFINED function (DCECheck_N) into every basic block. The compiler cannot inline or analyze a body it does not have, so it may remove the call only by proving the whole block unreachable — marker presence in the emitted assembly is an exact, black-box verdict on the block’s liveness as judged by the compiler.

  3. Ground truth by execution new

    Because programs are deterministic, RUNNING the instrumented binary reveals the true liveness: executed markers are alive; every other marker is genuinely dead. This is the "ideal compiler" baseline — what a perfect DCE with perfect analyses would eliminate.

  4. Differential comparison new

    Compile with multiple compilers (GCC, LLVM), multiple optimization levels (O0–O3, Os), and multiple versions; grep each binary for surviving marker calls. A dead marker eliminated by one configuration but kept by another is a missed optimization in the keeper — including the striking case where -O1 succeeds and -O3 fails in the SAME compiler.

  5. Primary-marker filtering + reduction new

    A missed block trivially causes its dead successors to be missed too; filtering keeps only PRIMARY misses — blocks whose predecessors are alive or correctly eliminated — pinpointing root causes. C-Reduce then shrinks each case (4–8 hours) into the small, reportable snippets that became 84 bug reports.

No compiler is modified at any step — the entire apparatus reads standard assembly output. The work received ASPLOS 2022 recognition including a distinguished artifact award.

The idea

Make optimization quality observable. The marker construction is the load-bearing trick. A call to a function with no visible body is an information black hole: the compiler must assume it has arbitrary side effects, so no transformation may delete it — except proving the enclosing block unreachable, in which case deleting it is mandatory for any DCE worth the name. The marker thus converts an internal, invisible judgment (“is this block dead?”) into a persistent, external artifact (a call instruction present or absent), checkable with a text search. Property one: black-box — no compiler internals touched. Property two: general — because DCE consumes the results of alias analysis, constant and value-range propagation, inlining, CFG simplification, and more, a surviving dead marker indicts whichever of those failed, whatever it was.

Let compilers testify against each other — and themselves. With liveness verdicts in hand, differential testing does the rest. GCC eliminates a marker LLVM keeps: LLVM missed something (and vice versa — 2,637 finds in GCC, 318 in LLVM suggest which pipeline currently reasons more completely about this program class). Subtler and more damning: the same compiler at -O1 eliminating a marker that -O3 retains — more optimization producing worse analysis, usually a pass-interaction bug. And across versions: 44 GCC and 38 LLVM cases where an older release out-optimized the current one — true regressions, bisected to 23 and 21 unique culprit commits touching alias analysis, call-graph handling, CFG analysis, peephole rules, loop optimizations, pass management, and value-range analysis. The breadth of that list is the generality claim, demonstrated.

Point at the root, not the rubble. If block B is missed, every dead block dominated by B is “missed” too — noise for a bug reporter. The primary-marker definition (a missed block all of whose predecessors are alive or correctly eliminated) collapses each failure cascade to its frontier, which is where the responsible analysis gave up. This is what makes the output reportable: 84 reduced test cases filed, 62 already confirmed or fixed by GCC and LLVM developers — the practical-relevance receipt few testing papers can show.

Quantify the ideal. Because ground truth comes from execution, the method also measures absolute optimizer quality against a hypothetical perfect-DCE compiler: at -O3, GCC eliminates 94.40% and LLVM 95.69% of dead blocks — leaving 1.53% and 1.37% as primary misses. The pipeline is good; the residue is thousands of real bugs.

One missed optimization, from random program to fixed compiler

Marker = call to an undefined function DCECheck_N, one per basic block. Dead = provably never executed. Primary miss = missed block whose predecessors are all handled correctly. The example mirrors the paper's Listings 1–3; bug numbers are from its reports.

Step 1 of 6

A random program with knowable dead code

Csmith emits a closed C program. Two branches happen to be dead: one guarded by if (d == e) where d and e hold addresses of different objects (never equal — but proving it needs alias reasoning), another by if (c) where c is initialized to 0 and never written (proving it needs constant propagation across the function).

dead branch 1: needs alias analysis
dead branch 2: needs constant propagation

Why it works

  • DCE sits downstream of most analyses → a dead block’s survival indicts whichever prerequisite failed → one simple oracle covers alias analysis to pass ordering, without naming any of them in advance.
  • Undefined-function calls are un-analyzable by construction → their presence in assembly is an exact, tamper-proof record of the compiler’s liveness judgment → the oracle is black-box, portable across compilers, and needs zero compiler modification.
  • Deterministic, inputless programs make liveness a fixed property → execution yields perfect ground truth → the method measures absolute quality (vs an ideal compiler), not merely relative disagreement.
  • Differential testing across compilers, levels, AND versions multiplies the oracle → -O1-beats-O3 finds pass-interaction bugs, version diffs find regressions — classes invisible to benchmarks.
  • Primary-marker filtering collapses failure cascades to their frontier → each find localizes a root cause → reduced cases are actionable, which is why developers confirmed or fixed 62 of 84.

What it costs

  • Random-program bias. Csmith code is not application code; misses are found where random control flow lands, and the 86%/10% dead/alive mix is unrepresentative of real programs (helpfully so, for stress-testing DCE).
  • DCE-visible misses only. Optimizations whose failure does not change block liveness — regalloc quality, vectorization width, scheduling — pass through the lens unseen.
  • A “miss” is relative. Differential findings show one configuration beating another, not that eliminating the block is cheap or that fixing it is worthwhile; human triage (and 4–8-hour C-Reduce runs per case) stands between finds and reports.
  • Marker perturbation. Inserting opaque calls inhibits some transformations around them; the measured pipeline is the instrumented program’s, slightly diverging from the pristine one.
  • C-family scope. Demonstrated on GCC/LLVM with C; the concept ports, but each new language/compiler needs its instrumenter and corpus.

The evaluation

The corpus: 10,000 Csmith-generated C files, 3,109,167 instrumented basic blocks, compiled by GCC and LLVM across optimization levels and versions. The study answers three questions: how good are the optimizers absolutely, where do they disagree, and what do the disagreements teach.

What the DCE lens revealed

PropertyAbsolute quality (vs ideal DCE)Cross-compiler differentialCross-level and cross-version differential
Headline numbers
Level scaling—Diminishing returns visible: O2→O3 moves the needle barely (5.66→5.60% GCC)
Practical outcome
CostBisection: hours per regression (compiler rebuilds)
Corpus: 10,000 deterministic Csmith C programs, 3.1 M instrumented blocks (89.59% dead / 10.41% alive by execution ground truth). Compilers: GCC and LLVM trunk plus historical versions, at -O0 through -O3 and -Os. Markers implemented as calls to undefined functions; liveness read from emitted assembly.Underlined values explain themselves when clicked.

Check your understanding

  1. 1. Why are the optimization markers implemented as calls to functions whose bodies are deliberately NOT provided?

  2. 2. Why is DCE, of all transformations, the right "lens" for finding misses across the whole compiler?

  3. 3. The corpus contains cases where -O1 eliminates a dead block that -O3 retains — in the same compiler. Why is this finding especially valuable?

  4. 4. When the tool finds ten consecutive missed dead blocks, primary-marker filtering reports only the first. What defines a primary miss, and why does the filtering matter?

Related papers on Compilers & Programming Languages

See all 162 →