What is dead-code elimination in the context of a JMH benchmark, and why can it make a microbenchmark report a misleadingly fast result?
answer
- Unused result -> JIT deletes the work
- Return the value or Blackhole.consume it
- Empty/near-zero score = suspect DCE
- JMH defends against the trap
- C2 proves result unobserved -> removes
basics
~20 sDead-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 sDead-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
Can state that unused results may be deleted by the JIT and knows to return the value from the benchmark.
Explains it as C2 dead-code elimination, knows both fixes (return value vs Blackhole), and recognizes a near-zero score as the symptom.
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.
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)