skip to content

What is constant folding in a benchmark, and how does the JMH @State pattern prevent the JIT from precomputing your inputs?

level: seniorimportance: must knowfreq 60%

answer

  1. Constant input -> JIT precomputes (folds)
  2. static final = compile-time constant, gets folded
  3. Read inputs from non-final @State field
  4. Two defenses: @State input + consumed output
  5. Folding hits inputs; DCE hits outputs

basics

~20 s

Constant folding is when the JIT precomputes an expression whose inputs are compile-time constants, so the benchmark never actually runs the work. Putting inputs in a non-final @State field that the JIT can't treat as constant forces the real computation to happen every time.

solid answer

~50 s

If a benchmark's inputs are constants the JIT can see (literals, or static final fields), C2 may fold the whole expression to its precomputed result — so you measure nothing. The defense is to read inputs from a JMH @State object's mutable instance fields. Because JMH constructs the @State per run and the values arrive from outside the compilation unit, the JIT cannot prove them constant and cannot fold the expression away; the work executes every invocation. Crucially the field should not be final or a compile-time constant: a static final primitive/String is a constant the JIT will inline and fold. So the pair of defenses is: take inputs from @State (defeats constant folding) and consume outputs via return/Blackhole (defeats dead-code elimination). Both are needed — defeating only one still yields a meaningless number. This is why idiomatic JMH benchmarks almost always read from @State fields rather than local literals.

code

java · 22 lines
java
import org.openjdk.jmh.annotations.*;

public class SqrtBench {

    // WRONG: literal input is constant-folded; result precomputed at compile time.
    @Benchmark
    public double folded() {
        return Math.sqrt(123.0);
    }

    @State(Scope.Thread)
    public static class Input {
        // Plain mutable instance field: NOT static final, so not a constant.
        double x = 123.0;
    }

    // RIGHT: input read from @State (no folding) + returned (no dead-code elimination).
    @Benchmark
    public double real(Input in) {
        return Math.sqrt(in.x);
    }
}

go deeper

for a junior

Aware that constant inputs can be precomputed and that JMH uses @State to hold inputs.

for a middle

Reads inputs from @State and outputs through return/Blackhole, and avoids literal constants as inputs.

for a senior

Explains constant folding vs dead-code elimination as distinct optimizations needing distinct defenses, and knows static final fields are folded so @State fields must be non-final.

for a principal

Can audit a benchmark end-to-end for folding, DCE, and other JIT effects, justify the @State scope chosen, and codify benchmarking standards so a team's numbers are reproducible and trustworthy.

## Background terms - **JIT / C2:** the JVM's optimizing compiler that turns hot bytecode into native code at runtime and applies aggressive optimizations. - **Constant folding:** a classic compiler optimization where an expression made of known-at-compile-time constants is evaluated *once, at compile time*, and replaced by its result. `2 * 21` becomes `42` in the compiled code; the multiply never runs. - **@State:** a JMH annotation on a class whose instance fields hold benchmark inputs/working data. JMH creates the @State object and passes it into your benchmark; its lifetime/sharing is set by a Scope (Thread/Benchmark/Group). ## The constant-folding trap Suppose you write: ```java @Benchmark public double measure() { return Math.sqrt(123.0); // input is a literal constant } ``` The argument `123.0` is a compile-time constant and `Math.sqrt` is pure, so C2 can compute `Math.sqrt(123.0)` once and replace the call with the literal result. Every invocation then just returns a precomputed number; you measure nothing about `sqrt`. The same happens with **static final** fields: ```java private static final double X = 123.0; // compile-time constant -> inlined & folded ``` A `static final` primitive or String is a constant expression the JIT will inline at the use site, then fold. ## Why @State fixes it Move the input into a *mutable* field of a @State object: ```java @State(Scope.Thread) public class Bench { double x = 123.0; // plain instance field, NOT static final @Benchmark public double measure() { return Math.sqrt(x); // x read from heap each time -> not a constant } } ``` Now `x` is an ordinary instance field. JMH constructs the @State object (often with values it could in principle change), and from the compiler's point of view the value lives on the heap and is not a provable constant within the compiled method. So C2 must actually load `x` and call `Math.sqrt(x)` on every invocation — the work happens for real. The key requirements: - The field must **not** be `final` initialized to a constant (and definitely not `static final`), or the JIT may treat it as constant again. - Reading it through the @State instance is what severs the 'constant' chain — the value comes from outside the optimizable region. ## The two-defense rule A trustworthy JMH benchmark needs BOTH defenses, because they block different optimizations: 1. **Inputs from @State** -> defeats **constant folding** (the JIT can't precompute the expression). 2. **Outputs returned or Blackhole-consumed** -> defeats **dead-code elimination** (the JIT can't delete the unused result). If you only consume the output but feed it a constant input, the whole result is folded to a constant and then 'consumed' as a constant — still meaningless. If you only take @State input but discard the output, DCE deletes the work. You need both. ## Quick diagnosis Suspiciously flat or near-zero times, or a score identical to a constant-return baseline, often mean folding (constant input) or DCE (unused output). Check that inputs come from a non-final @State field and outputs are consumed.

  • You read the input from a @State field but the field is declared 'static final double x = 123.0'. Does that defeat folding?
    No. A static final primitive is a compile-time constant the JIT inlines and folds, so the input is still constant. The field must be a non-final (plain) instance field so the value is a heap load the JIT cannot prove constant.
  • If you fix constant folding with @State but still return void, what happens?
    Dead-code elimination kicks in: the now-real computation produces a result nobody observes, so the JIT deletes it. You must also return the result or pass it to a Blackhole. Both defenses are required.

saying these in an interview costs you the question

  • Using static final or literal constants as benchmark inputs and trusting the result
  • Believing @State alone is enough while still discarding the output (DCE still applies)
  • Making the @State field final with a constant initializer, re-enabling folding
  • Thinking constant folding and dead-code elimination are the same optimization

context