What do constant folding, constant propagation, and dead-code elimination do inside a just-in-time compiler, and how does each pass create new work for the others?
answer
- SSA graph → folding, propagation, DCE, iterate
- constant lattice: unknown / one constant / not constant
- folded condition → whole branch arm deleted
- side effects and possible exceptions are never dead
- javac folds constant variables; JIT folds runtime facts
basics
~20 sConstant folding evaluates expressions whose operands are already known at compile time. Constant propagation substitutes those known values into later uses. Dead-code elimination deletes computations nobody reads and branches that can never be taken. Each pass exposes fresh opportunities for the others.
solid answer
~60 sThese are three of the oldest optimizing-compiler passes, and a JIT runs them repeatedly over its intermediate representation. - **Constant folding** evaluates an operation whose operands are known values and replaces it with the result: `60 * 60 * 1000` becomes `3600000`. - **Constant propagation** rewrites every downstream use of a variable that provably holds a single known value into that literal, which usually creates newly foldable expressions. - **Dead-code elimination (DCE)** deletes any node whose result is never consumed by a side effect, a return, or a live branch — including a whole branch arm whose condition folded to a constant. They are mutually reinforcing, so the compiler iterates to a fixed point: folding feeds propagation, propagation folds a comparison to `true`, DCE drops the untaken arm, that removes the last use of another computation, DCE drops that too. A JIT is stronger here than the source compiler because after class loading and inlining it knows runtime facts — an initialized `static final` value, a configuration flag read once, an actual receiver type — that were not constants in the source text.
go deeper
Define each of the three passes in one sentence and give a tiny example of folding an arithmetic expression and deleting an unused variable.
Show the chain: inline, propagate, fold a condition, delete the untaken arm, delete the now-unused computation — and name what may never be deleted.
Bring in SSA, the constant lattice with merge points, and the fact that runtime facts become compile-time constants only under an assumption the JVM must be able to back out of.
Frame it as a budget question: DCE and folding shrink code, which buys inlining headroom and i-cache locality, and explain how you would decide whether an observed speedup is real work removed or work proven unobservable.
## Where these passes live HotSpot interprets bytecode first and compiles hot methods with an optimizing compiler (C1 or C2). The compiler translates bytecode into an internal graph in **SSA form** — static single assignment, meaning every value is written exactly once and each use points directly at the node that produced it. SSA is what makes these classic passes cheap: to know what a use holds, you follow one edge to its definition rather than searching the program. ## Constant folding Folding evaluates, at compile time, any operation whose inputs are literal values, and replaces the operation node with the result. `24 * 60 * 60` becomes `86400`; `"a" + "b"` on constants becomes `"ab"`; `Integer.valueOf(5).intValue()` can fold to `5` once the calls are inlined. Folding must respect exact language semantics: integer overflow wraps the same way the hardware would, division by a constant zero cannot be folded into a value because it must still throw `ArithmeticException`, and floating-point folding must reproduce IEEE-754 results bit for bit, so the compiler evaluates with the same rounding the target would use. Note that some folding already happened before the JVM ever ran the code. The Java Language Specification defines *constant variables* — `static final` primitives and `String`s with a constant initializer — and `javac` inlines their values into the callers' class files. The JIT's folding is the runtime continuation of that: it can fold things `javac` could not, because it sees loaded classes and inlined callee bodies. ## Constant propagation Propagation answers "what does this variable hold here?" and rewrites uses accordingly. In SSA, a value produced by a constant node is trivially propagated to every use. The interesting case is a merge point: if control flow joins from two paths that both produce `7`, the merge still holds `7`; if one produces `7` and the other `9`, it holds neither and propagation stops. This is the classic *sparse conditional constant propagation* lattice — each value is either unknown, exactly one constant, or definitely non-constant. Conditional propagation is the powerful form: it prunes edges as it goes, so a branch already proven untaken never contributes its values to a merge, letting more variables stay constant than a naive analysis would allow. ## Dead-code elimination DCE runs the liveness question backwards. Start from the roots that must be preserved — returns, thrown exceptions, stores to memory, calls that might have side effects, and the state needed to reconstruct an interpreter frame if the code deoptimizes — and mark everything reachable through use edges. Whatever is unmarked is deleted. Two shapes matter in practice: 1. **Unused results.** A pure computation (arithmetic, a field read the compiler can prove has no side effect, an object allocation that escape analysis proved is confined) whose result nobody reads simply disappears. 2. **Unreachable code.** When a condition folds to a constant, the untaken arm is unreachable and is removed wholesale, along with any code only that arm used. DCE cannot remove anything the JVM must still observe. A field write to a shared object stays. A method call the compiler cannot prove pure stays. And a check that could throw — a null check, a bounds check, a class cast — stays unless the compiler proves it can never fire, because the exception is an observable effect. ## Why the passes are run in a loop Each pass is a source of inputs for the others, so the compiler iterates until nothing changes: - inlining a small method exposes its argument as a known constant; - propagation pushes that constant into a comparison; - folding turns the comparison into `true`; - DCE deletes the `false` arm; - that deletion removes the last use of a computation, which DCE also removes; - the now-simpler graph may expose the next constant. This chain is why a heavily parameterised method, once inlined at a call site with fixed arguments, can compile down to a handful of instructions — and it is the same chain that makes a naive timing harness report near-zero cost for a computation whose result is thrown away. The compiler is not cheating; it simply proved the work was unobservable. ## What this buys Folding and propagation shrink the instruction count and free registers. DCE shrinks code size, which improves instruction-cache behaviour and lets the compiler afford more inlining within its budget. Together they are the reason JIT-compiled code is often compared with statically compiled C: the passes are the same textbook passes, run against better information because they run after the program has started.
- Why can the JIT fold expressions that javac could not?javac sees only the source of one compilation unit and may fold only what the JLS calls constant variables. The JIT runs after classes are loaded and after inlining, so it knows the actual receiver type, the value stored in an initialized final field, and the arguments at a particular call site. Those runtime facts become compile-time constants for the method being compiled, at the cost of having to deoptimize if an assumption later breaks.
- Name a computation the JIT is not allowed to delete even though its result is unused.Anything with an observable effect: a store to a field or array that another thread could read, a synchronized or volatile operation, a call the compiler cannot prove side-effect-free, and any operation that could throw — a division that could divide by zero, an array access that could go out of bounds, a null dereference. The exception itself is the observable result, so the check survives even when the value does not.
saying these in an interview costs you the question
- Claiming the JIT deletes code that has side effects or could throw, as long as the value is unused
- Thinking constant folding is only a javac feature and the JIT just translates bytecode
- Assuming floating-point expressions are folded loosely, ignoring exact IEEE-754 reproduction
- Saying dead-code elimination is about unreachable source lines only, missing unused-result elimination
- Describing the passes as running once, in a fixed order, rather than iterating to a fixed point