skip to content

What is dead-code elimination in the context of a JMH benchmark, and why can it make a microbenchmark report a misleadingly fast result?

level: juniorimportance: must knowfreq 70%

answer

  1. Unused result -> JIT deletes the work
  2. Return the value or Blackhole.consume it
  3. Empty/near-zero score = suspect DCE
  4. JMH defends against the trap
  5. C2 proves result unobserved -> removes

basics

~20 s

Dead-code elimination is when the JVM deletes code whose result is never used. In a benchmark, if you compute something but never use the value, the JIT may delete the work, so the benchmark times nothing and reports an impossibly fast result.

solid answer

~40 s

Dead-code elimination (DCE) is a standard compiler optimization: if a computation's result is never observed, it has no effect on the program, so the compiler is free to delete it. In a JMH microbenchmark this is a trap. If your @Benchmark method computes a value but discards it, the JIT (HotSpot's C2 compiler) can prove the result is unused and remove the whole calculation. The benchmark then measures an empty method and reports a wildly fast, meaningless score (often near the cost of an empty loop). The fix is to make the result observable: return it from the @Benchmark method so JMH consumes it, or pass it to a Blackhole. JMH was built specifically to make these defenses easy, because hand-written benchmarks routinely fall into this trap.

go deeper

for a junior

Can state that unused results may be deleted by the JIT and knows to return the value from the benchmark.

for a middle

Explains it as C2 dead-code elimination, knows both fixes (return value vs Blackhole), and recognizes a near-zero score as the symptom.

for a senior

Reasons about why DCE is correct for real code but a hazard for benchmarks, and can pick return-vs-Blackhole based on number of results and loop structure.

for a principal

Frames JMH's whole design (return-consume, Blackhole, @State) as defenses against JIT optimizations, and can audit a benchmark for all the traps (DCE, constant folding, loop unrolling) before trusting its numbers.

## What is a benchmark and why does this problem exist A **microbenchmark** measures how long a small piece of code takes to run. **JMH** (Java Microbenchmark Harness) is the standard OpenJDK tool for writing them; you annotate a method with `@Benchmark` and JMH calls it many times and reports the average time. The **JIT compiler** ('Just-In-Time') is the part of the JVM that, while your program runs, translates hot bytecode into optimized native machine code. HotSpot has two JIT compilers: **C1** (fast, lightly optimized) and **C2** (slower to compile but heavily optimized). C2 applies aggressive optimizations — and the one that breaks benchmarks is **dead-code elimination**. ## What dead-code elimination (DCE) is **Dead code** is code whose result is never used and that has no observable side effect. A compiler is allowed to *delete* dead code because removing it cannot change the program's output — that is the whole point of the optimization, and it is correct for real programs. Example of dead code: ```java int x = expensiveComputation(); // x is never read again // ... x unused ... ``` Since `x` is never observed, `expensiveComputation()` could be removed entirely (assuming it has no side effects). ## Why it ruins a benchmark In a *real* program, you compute things to use them, so DCE rarely deletes anything important. But in a *benchmark* you often compute something purely to time it and then throw the result away — which is exactly the pattern DCE targets. Consider: ```java @Benchmark public void measure() { Math.log(value); // result discarded } ``` C2 sees the result of `Math.log` is never used, proves it has no side effect, and deletes the call. Your `@Benchmark` method becomes effectively empty. JMH then reports a time close to the cost of an empty method call (nanoseconds or less) — a number that has nothing to do with `Math.log`. You'd conclude `Math.log` is essentially free, which is false. ## How to defend against it The rule: **make every result observable** so the compiler cannot prove it is dead. 1. **Return the value.** If a `@Benchmark` method returns its result, JMH takes the returned object and feeds it into a Blackhole internally, so the JIT must actually produce it: ```java @Benchmark public double measure() { return Math.log(value); // returned -> consumed by JMH } ``` 2. **Use a `Blackhole`** when you produce more than one result or a result mid-loop. A Blackhole is a JMH-provided 'sink' you pass values to; it is engineered so the JIT cannot optimize the consumption away: ```java @Benchmark public void measure(Blackhole bh) { bh.consume(Math.log(value)); bh.consume(Math.exp(value)); } ``` ## Key takeaway DCE is a *correct* optimization that becomes a *measurement hazard* in benchmarks. JMH exists largely to give you the tools (returning values, `Blackhole`) to stop the JIT from deleting the very work you are trying to measure. If a benchmark reports a suspiciously tiny or zero time, suspect DCE first.

  • If returning the value already defeats DCE, why does Blackhole exist at all?
    A @Benchmark method can return only one value. When you produce several results, or a result inside a loop, or want to time a void operation, you need a sink for each — Blackhole.consume() lets you sink any number of values without the JIT folding or eliminating them.
  • How would you notice DCE happened just from the JMH output?
    The score is implausibly low — often the same as a baseline empty benchmark, or near the timer resolution. JMH also prints a warning hinting your benchmark may be optimized away if it can detect it.

saying these in an interview costs you the question

  • Thinking DCE is a JMH bug rather than a correct general compiler optimization
  • Believing returning void from a benchmark is fine when you compute a single result
  • Concluding a method is 'basically free' from a near-zero score without checking for DCE
  • Assuming -Xint or disabling the JIT is the right fix (it makes the benchmark unrepresentative)

context