Guards
The cheap runtime check that turns an assumption into a sound one. A guard is a comparison, a branch and a piece of metadata — and its cost is the bar every speculation has to clear before it is worth making.
What does the check in front of a speculative fast path actually cost, and what does it have to do besides compare two values?
A guard is an IR instruction with two unusual properties: it has no result, and it has an implicit edge out of the function to a reconstruction point. That gives the IR a shape ordinary instructions do not have — a linear fast path with exits hanging off it, each exit annotated with the abstract machine state to resume from. The representation exists to answer a question a plain branch cannot: not "which way does control go" but "under what condition is everything after this point valid, and where does execution resume if it is not".
A guard is correct only if it is *sufficient* for the assumption its downstream code relies on — testing a necessary-but-not-sufficient condition is the classic way to produce a fast path that runs on values it was not written for. It must be placed so that no observable effect of the guarded region precedes it, so the fallback can perform the general operation from a clean state. It must itself be free of observable effects and unable to trap. And a guard may be hoisted out of a loop or merged with another only if the value it tests is provably unchanged between the original location and the new one — hoisting a guard on a value the loop modifies removes exactly the check that made the loop body legal.
Key points
- A guard is a compare and a branch to an out-of-line reconstruction stub; everything else about it is metadata.
- The check must be sufficient for the assumption, not merely correlated with it, or the fast path runs on values it was not written for.
- It must precede every observable effect of the guarded region, so that the fallback can perform the general operation from a clean state.
- The real cost is not the comparison but the state map: every value it names must stay recoverable, which constrains the optimizer around it.
- Hoisting a guard out of a loop is what makes many speculations worthwhile, and is legal only if the guarded value is provably unchanged across the range.
- A guard's cost is the bar the speculation must clear, which is why the ratio of work-removed to check-cost decides what engines speculate on.
- Fewer and cheaper guards is a bigger lever than a faster fast path, because each guard removed also removes a state map and its liveness obligations.
- The compactness of the state-map format effectively sets how much a compiler can afford to speculate.
What a guard is, concretely
adds/b.vs pair, and an engine using NaN-boxing rather than pointer tagging would test entirely differently. The structure — hoisted checks outside, an overflow exit inside, cold reconstruction out of line — survives all of those; none of the instructions do.Stripped of vocabulary, a guard is two machine instructions: a compare and a conditional branch to an out-of-line stub. Everything else about it is metadata. It produces no value, it is not part of the computation, and in the common case where the check passes, it costs a comparison against a register or an immediate and a branch the predictor learns after two executions.
What makes it more than a branch is what hangs off the not-taken side. The exit does not go to an else block; it goes to a reconstruction sequence that materializes an interpreter frame and resumes at a specific bytecode offset. That target has to be described by compiler-emitted metadata, and the existence of that description is what constrains everything around the guard — see [[deoptimization]].
The metadata is also where most of the cost actually is. The compare is one cycle; the state map naming every live value at that point is memory, and worse, it is an obligation: every value the map names must still be recoverable at that point in the optimized code, which forbids the optimizer from destroying it. A function with many guards has many state maps, and their combined demands on liveness are a real constraint on register allocation and on what may be optimized away.
1 ; guard, once, before the loop2 test rax, 1 ; small-integer tag test on the accumulator3 jnz .deopt_offset_12 ; not tagged as a small integer -> reconstruct and resume4 test rbx, 15 jnz .deopt_offset_126 7.loop:8 add rax, rbx ; no type test, no boxing, no dispatch9 jo .deopt_offset_14 ; overflow leaves for the general path10 add rbx, 211 cmp rbx, rcx12 jl .loop13 14.deopt_offset_12: ; out of line, cold, never in the loop's cache footprint15 ; materialize interpreter frame from the state map, resume at bytecode 12Four instructions of checking outside the loop protect a body with none inside it. The overflow exit is a guard too, and it is the one people forget: the operand check established the representation and said nothing about the result. Note also where the reconstruction sequence lives — out of line, so the hot loop's instruction footprint does not include it.
The bar the speculation has to clear
A guard is the price of admission, and the reason it is worth naming as its own lesson is that the price sets the policy. A speculation is worth making when the work removed exceeds the check that permits it, so the interesting question at every candidate site is a ratio.
Replacing a dictionary lookup with a fixed-offset load in exchange for one pointer comparison is an enormous ratio, which is why shape guards are everywhere. Replacing a virtual dispatch with a direct call in exchange for one comparison is a better ratio still, because the direct call then enables inlining. Replacing a generic addition with a machine add in exchange for two tag tests is a good ratio *only if the tests can be hoisted* — two tests per iteration against one add per iteration is a much less attractive trade than two tests per loop.
Which is why hoisting is not an optimization applied to guards afterwards but part of what makes them viable at all. A guard on a loop-invariant value belongs outside the loop; a guard on a value the loop changes cannot move, and speculation on that value is correspondingly less valuable. The classic instance is [[loop-invariant-code-motion]] applied to a check rather than to a computation, and the legality condition is the same one: the value must be provably unchanged across the range the guard is being moved over.
| Guard | Cost when it passes | What it permits | Hoistable? |
|---|---|---|---|
| Small-integer tag test | One test and a well-predicted branch | Unboxed register arithmetic instead of generic dispatch | Yes, if the value is loop-invariant or its definition dominates the loop |
| Shape / hidden-class check | One load and one pointer comparison | A fixed-offset field load instead of a lookup | Yes, when the object reference does not change |
| Call-target check | One comparison against a known target | A direct call, and therefore inlining and everything downstream | Usually, for a loop-invariant receiver |
| Null checkimplementation | Often zero — a trapping memory access serves as the check | Removal of explicit null tests throughout the region | Yes; this is the classic hoisted check |
| Overflow check on the fast path | One flag test per operation | A machine add where the language specifies promotion on overflow | No — it is a property of each result, not of the inputs |
| Global-invariant dependency | Nothing at all at run time | Constant folding through a runtime value | n/a — there is no per-execution check to hoist |
How guards go wrong
Three failure shapes account for nearly all guard bugs, and each is a violation of one clause of the legality condition.
Insufficient: the guard checks something implied by the assumption rather than something that implies it. "This is an object" does not establish "this object has property x at offset 16". A value that passes takes a fast path written for a different value, and the result is a wrong answer with no error anywhere — the worst outcome available in this domain, and the reason engines run interpreted and compiled results against each other in testing.
Late: the guard is placed after work that is already observable. Now the fallback cannot cleanly redo the general operation, because part of it has happened: a getter ran, a counter incremented, an exception was thrown from a state the interpreter does not expect. The program is not wrong on the fast path and is wrong on the recovery path, which means it is wrong only for inputs that fail the guard — a defect that survives every test using well-behaved input.
Wrongly hoisted: the guard is moved out of a loop over a value the loop modifies. It passes once, and the body runs for the rest of the loop on values that were never checked. This is a plain violation of the motion legality condition, and it is easy to introduce because the code looks correct: the check is present, it is just in the wrong place.
- A guard that is necessary but not sufficient produces silent wrong answers, not crashes.
- A guard placed after an observable effect produces a correct fast path and an incorrect recovery path.
- A guard hoisted over a modification protects only the first iteration.
- A guard that can itself trap or allocate is not a guard — the check must be cheaper and simpler than the thing it protects, or the design has inverted.
- A guard whose state map is incomplete produces a reconstruction that resumes with wrong local values, which looks like memory corruption and is not.
Guards are the shape of the whole system
It is worth stepping back, because the guard is where the module's two halves meet. Looking forward from it, a guard is what makes [[speculative-optimization]] legal — the mechanism by which evidence becomes soundness. Looking backward from it, a guard is what makes [[deoptimization]] necessary — every guard is a potential exit, and every exit needs somewhere to land.
That double role explains an otherwise puzzling design pressure in real engines: enormous effort goes into having *fewer, cheaper, more hoistable* guards rather than into making the fast path faster. Merging two guards that check overlapping conditions, proving one guard subsumes another, hoisting a check out of a loop, replacing an explicit null test with a trapping load — each removes a check from the hot path and, just as importantly, removes a state map and the liveness obligations that came with it.
And it explains why "how much can this JIT speculate" is in practice "how cheaply can this JIT describe its state". An engine that can encode a state map in a few bytes can afford guards densely; one whose maps are expensive must be conservative about where it puts them, and therefore about what it dares to assume. The performance ceiling of a speculative compiler is set by its metadata format, which is not where anyone expects to find it.
How it works
The steps, in the order the compiler takes them.
- For each speculative specialization, determine the weakest condition that is still sufficient for every assumption the specialized code makes.
- Emit a guard testing that condition, placed at the earliest point that dominates the specialized region and before any observable effect within it.
- Attach the bytecode offset and a state map naming every value the interpreter would need at that offset.
- Mark the guard as having an implicit exit edge, so that later passes treat it as a use of every value the state map names.
- Run guard-specific simplification: merge guards testing the same condition, remove a guard subsumed by an earlier one, and hoist loop-invariant guards to the loop preheader.
- Lower each guard to a compare and a conditional branch to an out-of-line stub, keeping the reconstruction sequence out of the hot path's instruction footprint.
- Where the target and runtime allow, replace an explicit check with a trapping access plus a fault-address-to-metadata map, so the passing case costs nothing.
- Record which guard failed when one does, so the profile can be widened and the next compilation can make a different bet.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A guard tests a necessary condition instead of a sufficient one, and a value that passes takes a fast path written for a different value — a wrong result, no error, and nothing in any trace to indicate a guard was involved.
- A guard is placed after an observable effect, so failing it re-executes that effect on the general path: a getter called twice, a value logged twice, or an exception raised from a program state the interpreter cannot make sense of.
- A guard is hoisted over a write to the value it checks, and every iteration after the first runs unchecked. The loop produces correct output for inputs where the value happens not to change.
- The state map attached to a guard omits a live value, and reconstruction resumes the interpreter with a stale or garbage local — presenting as memory corruption far from the guard.
- Guards accumulate in a hot loop because none is hoistable, and the specialized code is slower than the generic version it replaced while every individual check looks cheap.
- An implicit null check relies on a trapping access, and the code is run in an environment where address zero is mapped or the signal is intercepted by something else, converting a guard into a genuine crash or a silent misbehaviour.
When it helps
- Anywhere the general operation is much more expensive than the check: property lookup, virtual dispatch, boxed arithmetic, dynamic type coercion.
- Hot loops where the checked value is loop-invariant, so one check protects an entire loop's worth of specialized body.
- Call sites where a single comparison unlocks inlining, which is the largest structural transformation available to any optimizer.
- Places where a runtime-wide invariant can be watched globally rather than checked locally, making the run-time cost of the assumption exactly zero.
- Reasoning about a JIT's output. Reading the guards in a disassembly tells you precisely what the compiler assumed, which is usually more informative than reading the fast path.
When it hurts
- Values that change every iteration, where the guard cannot be hoisted and is paid at the same frequency as the operation it protects.
- Sites with many independent assumptions, where the guards accumulate into a prologue that costs more than the specialization saves.
- Code where the state maps are the binding constraint: many guards means many maps, and their liveness requirements can keep values alive that the optimizer would otherwise have removed.
- Environments where the cheapest guard techniques are unavailable — no signal-based null checks, no usable overflow flag, no tag bits in the value representation.
- Security-sensitive code, where the timing difference between the guarded fast path and the general path is observable and the branch itself is a signal.
What it costs
Every one of these is paid by something.
- A guard buys the legality of an entire specialized region and pays a compare and a branch on every execution, plus an out-of-line stub in the code footprint.
- Attaching a state map buys a recoverable exit and pays by pinning every value the map names — the optimizer may not delete, fold away or fail to materialize any of them at that point.
- Hoisting a guard buys per-iteration speed and pays with a wider failure scope: failing a hoisted guard invalidates an entire loop's assumption rather than one iteration's.
- Implicit trapping checks buy a zero-cost passing case and pay with a dependency on operating-system signal handling, an unmapped guard page, and a fault-address-to-metadata table that must be exactly right.
- A stronger, more specific guard buys a more aggressive fast path and pays with a higher failure rate, since a narrower condition is violated more often.
What else you could do
What a different compiler or language does instead, and when that is better.
- Prove the condition instead of checking it. A sound static type, a sealed class or a whole-program analysis removes the guard entirely —
[[whole-program-optimization]]and[[monomorphization]]. - Record a global dependency rather than emitting a per-execution check, so the runtime invalidates the code if the invariant breaks. Zero run-time cost, at the price of a runtime-wide invalidation mechanism.
- Check once at a boundary rather than at every use — the contract-checking approach of gradual typing, where a value is validated as it crosses into typed code and trusted thereafter —
[[gradual-typing]]. - Use an inline cache instead of a compiled-in guard: check the cached key per execution but do not recompile the surrounding code around the assumption, so a miss costs a lookup rather than a deoptimization —
[[inline-caches]]. - Do not specialize. The generic operation needs no guard, is never wrong, and is the right answer at genuinely polymorphic sites.
See it for yourself
The flag, dump or tool that shows you this directly.
- HotSpot:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblywith the hsdis disassembler shows guards as real compare-and-branch pairs with the uncommon-trap stubs they jump to. - V8:
--print-opt-codeprints the optimized code including the deoptimization exits, and--trace-deoptreports which guard fired and at what bytecode offset. - HotSpot:
-XX:+TraceDeoptimizationon a debug build names the reason, which is effectively a list of the guard kinds the engine emits. - JITWatch overlays HotSpot's compiled output on the bytecode, which makes it possible to see which source-level operation each guard is protecting.
- For the general discipline rather than any engine, our transform explorer at
/compilers/optimizeshows every transformation with its precondition, which is the same argument a guard is making at run time instead of compile time.
Plausible wrong readings
Stated the way a confident engineer states them.
- "A guard is just an if statement." It is a branch with an implicit exit out of the function to a reconstruction point, and it carries a description of the abstract machine state at that point. The metadata is the expensive half.
- "Guards make the code slower, so a good JIT minimizes them." A good JIT minimizes them, and not because they are slow — because each one is a state map and a set of liveness obligations. The comparison itself is nearly free.
- "If the guard passes, nothing was paid." The comparison and branch were paid, and more importantly the optimizer paid: values were kept alive and effects were not moved, because the guard might have failed.
- "A stronger guard is a safer guard." A stronger guard fails more often, and failure is thousands of times more expensive than the check. The right guard is the weakest one still sufficient for the assumption.
- "A null check always costs an instruction." On a runtime that can use a trapping load and a signal handler, the passing case costs nothing at all and the cost is entirely in the metadata that maps the fault address to a resume point.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
Before running fast code that assumes something, the compiler puts in a quick test of that assumption. If the test passes, the fast code runs. If not, the program leaves for a slower path that handles the general case. The test is usually one comparison, and the branch is one the processor learns to predict almost immediately, so in the normal case it costs very little.
practical
When reading a JIT's output, read the guards first. They tell you exactly what the compiler assumed, which is more informative than the fast path and explains most deoptimizations you will ever chase. Two patterns are worth recognising: guards in the loop preheader mean the compiler proved the value invariant and hoisted them, which is the good case; a pile of guards inside a loop body means it could not, and the specialization may be barely paying for itself. And when a guard fails repeatedly, the fix is almost never to make the guard cheaper — it is to make the code stop violating the assumption.
advanced
The non-obvious consequence of guards is that they turn the optimizer into a two-observer problem. Ordinarily an optimizer must preserve the program's defined observable behavior and is otherwise free. With guards it must additionally keep every state map satisfiable, which makes each map a second observer of the intermediate state. This is why a JIT declines transformations an ahead-of-time compiler performs routinely: a dead-value elimination that would be plainly legal is forbidden because a state map names that value, and a code motion that preserves behavior is forbidden because it crosses a guard. The practical response is a body of engineering aimed squarely at reducing the number and weight of these observers — guard merging, subsumption, hoisting, and above all compact state-map encodings. It is a genuinely unusual situation in compiler design: the format of a metadata table is a first-order determinant of how fast the generated code can be.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
If you were asked this in an interview
- What is the difference between a guard and an ordinary conditional branch?
- A guard checks that a value is an object before loading a field at a fixed offset. What is wrong with that?
- When may a guard be hoisted out of a loop, and what breaks if the condition is not met?
- Why does the encoding of deoptimization metadata affect how fast the generated code can be?
Connections
- Programming Languages & Runtime Internals — Value representation — tag bits, NaN-boxing, hidden-class pointers — which is what a guard is actually comparingA guard is a comparison against a representation the runtime defines. Whether a small-integer test is one bit test, a range check on a double, or a load and compare against a shape pointer is decided by the object model, and the object model therefore sets the price of every speculation the compiler wants to make.