| name | click-jvm-optimization |
| description | Design JIT compilers and optimize managed runtimes in the style of Cliff Click, architect of the HotSpot JVM C2 compiler and creator of sea-of-nodes IR. Emphasizes advanced compiler optimizations, intermediate representation design, and making dynamic languages fast. Use when building JIT compilers, VMs, or working on compiler backends. |
| tags | jit, jvm, hotspot, optimization, compiler, garbage-collection, escape-analysis, inlining, performance, sea-of-nodes |
Cliff Click Style Guide
Overview
Cliff Click was the chief architect of the HotSpot Server Compiler (C2), which made Java competitive with C++ for server workloads. He invented the sea-of-nodes intermediate representation and pioneered optimization techniques used in most modern JIT compilers. His work proved that dynamic languages could be fast.
Core Philosophy
"Optimization is about proving things don't happen."
"The best optimization is the one that removes code entirely."
"A good IR makes optimizations fall out naturally."
Click believes that compiler optimization is fundamentally about building proofs—proving that code can be simplified, proving that operations can be reordered, proving that entire computations can be eliminated.
Design Principles
-
Sea of Nodes IR: Data dependencies, not artificial instruction order.
-
Speculative Optimization: Optimize for the common case, deoptimize when wrong.
-
Type Speculation: Dynamic types can be optimized like static types.
-
Escape Analysis: Prove objects don't escape, eliminate allocations.
When Building Compilers
Always
- Design IR for the optimizations you want
- Preserve information needed for later passes
- Make deoptimization fast and correct
- Profile to guide optimization decisions
- Inline aggressively (with limits)
- Prove properties rather than assuming them
Never
- Lose information during lowering too early
- Optimize without profiling data
- Make deoptimization expensive
- Assume optimization order doesn't matter
- Skip escape analysis for object languages
- Ignore memory aliasing
Prefer
- Sea-of-nodes over linear IR for optimization
- Speculative optimization with guards
- Type feedback over static analysis alone
- Global value numbering over local CSE
- Loop transformations that enable vectorization
- Incremental compilation over batch
Code Patterns
Sea of Nodes IR
class Node {
int opcode;
Node[] inputs;
Node[] outputs;
}
Global Value Numbering
class ValueNumbering {
Map<NodeKey, Node> valueNumbers = new HashMap<>();
Node idealize(Node n) {
NodeKey key = canonicalize(n);
Node existing = valueNumbers.get(key);
if (existing != null) {
return existing;
}
valueNumbers.put(key, n);
return n;
}
NodeKey canonicalize(Node n) {
if (isCommutative(n.opcode)) {
sortInputs(n);
}
return new NodeKey(n.opcode, n.inputs);
}
}
Speculative Optimization with Guards
void compileCallSite(CallSite site, ProfileData profile) {
if (profile.isMonomorphic()) {
Class<?> observedType = profile.getObservedType();
emitTypeCheck(observedType);
emitDirectCall(observedType);
emitDeoptimize();
} else if (profile.isBimorphic()) {
emitTypeSwitch(profile.getTypes());
} else {
emitVirtualCall();
}
}
Escape Analysis
class EscapeAnalysis {
boolean canEliminate(AllocationNode alloc) {
Set<Node> uses = alloc.getTransitiveUses();
for (Node use : uses) {
if (escapes(alloc, use)) {
return false;
}
}
return true;
}
boolean escapes(AllocationNode alloc, Node use) {
if (use instanceof StoreField) {
StoreField sf = (StoreField) use;
return sf.getObject() != alloc;
}
if (use instanceof Call) {
Call call = (Call) use;
return !isInlinedCall(call);
}
return false;
}
}
Loop Optimizations
class LoopOptimizations {
void optimizeLoop(LoopNode loop) {
for (Node n : loop.getBody()) {
if (isLoopInvariant(n, loop)) {
moveBeforeLoop(n, loop);
}
}
InductionVar iv = findInductionVariable(loop);
if (canEliminateRangeCheck(loop, iv)) {
hoistRangeCheck(loop, iv);
}
if (shouldUnroll(loop)) {
unroll(loop, UNROLL_FACTOR);
}
if (canVectorize(loop)) {
vectorize(loop);
}
}
}
Deoptimization Infrastructure
class Deoptimization {
static class DeoptInfo {
int bci;
Object[] locals;
Object[] stack;
Object[] monitors;
}
void deoptimize(DeoptInfo info) {
InterpreterFrame frame = new InterpreterFrame();
frame.setBCI(info.bci);
frame.setLocals(info.locals);
frame.setStack(info.stack);
for (Object monitor : info.monitors) {
monitorEnter(monitor);
}
interpreter.execute(frame);
}
}
Type Feedback System
class TypeProfile {
TypeProfileEntry[] receivers = new TypeProfileEntry[2];
int count;
void recordType(Class<?> type) {
for (int i = 0; i < count; i++) {
if (receivers[i].type == type) {
receivers[i].count++;
return;
}
}
if (count < receivers.length) {
receivers[count++] = new TypeProfileEntry(type, 1);
} else {
morphism = MEGAMORPHIC;
}
}
OptimizationHint getHint() {
if (count == 1 && receivers[0].count > THRESHOLD) {
return new Monomorphic(receivers[0].type);
}
if (count == 2) {
return new Bimorphic(receivers[0].type, receivers[1].type);
}
return MEGAMORPHIC;
}
}
Register Allocation
class RegisterAllocator {
void allocate(Graph graph) {
InterferenceGraph ig = buildInterferenceGraph(graph);
Map<Node, Integer> coloring = colorGraph(ig);
while (coloring == null) {
Node toSpill = selectSpillCandidate(ig);
insertSpillCode(toSpill);
ig = rebuild(ig, toSpill);
coloring = colorGraph(ig);
}
for (Map.Entry<Node, Integer> e : coloring.entrySet()) {
e.getKey().setRegister(physicalRegister(e.getValue()));
}
}
}
JIT Compilation Philosophy
Tiered Compilation Strategy
══════════════════════════════════════════════════════════════
Tier Compiler Optimization When Used
────────────────────────────────────────────────────────────
0 Interpreter None First execution
1 C1 (Client) Light Moderate hotness
2 C2 (Server) Aggressive Very hot methods
Compilation triggers:
- Method entry count threshold
- Loop back-edge count threshold
- On-stack replacement for hot loops
Key insight: Most code is cold
Compile only what matters
Quick startup, peak performance eventually
Mental Model
Click approaches compiler design by asking:
- What can I prove? Optimizations are proofs
- What's the common case? Speculate on it
- What information do I need? Preserve it in the IR
- What can be eliminated? The fastest code is no code
- How do I recover when wrong? Deoptimization must work
Signature Click Moves
- Sea-of-nodes IR: Dependencies, not artificial order
- Speculative optimization: Bet on the common case
- Escape analysis: Eliminate allocations entirely
- Global value numbering: One computation per value
- Profile-guided optimization: Runtime feedback guides compilation
- Tiered compilation: Quick startup, peak performance later
- Deoptimization: Safe return to interpreter
- Loop optimizations: Range checks, unrolling, vectorization