Code Says X → Compiler Thinks Y
The searchable index from a line of source to the transformation, representation or decision it triggers — and why.
1 + 2→Evaluate it now and emit the literal 3.Both operands are compile-time constants and integer addition on them has one answer that does not depend on the target or on anything that happens at run time, so evaluating it during compilation changes nothing the program can observe — it only moves work earlier.
x + 0 where x is an integer→The addition is an identity. Use x directly.Adding zero to any two’s-complement integer yields the same bit pattern, so the instruction cannot change the value or set any flag the language exposes. The rewrite is an algebraic identity that holds for every input, which is what distinguishes it from a guess about the common case.
f + 0.0 where f is a float→Leave it alone. This is not an identity.Under IEEE 754, negative zero plus positive zero is positive zero, so the addition changes an observable value for one input. It also quiets a signalling NaN. Compilers only remove it when a flag has explicitly traded IEEE semantics away, which is a change to the language, not an optimization within it.
x * 8→Shift left by three.Multiplication by a power of two is a left shift for unsigned and two’s-complement operands, and shifts have historically been the cheaper instruction. Strength reduction is a family of these substitutions; whether the shift is actually faster is a property of the target, which is why the decision belongs to instruction selection rather than to the middle end.
let t = a * b; and t is never read again→Delete the multiplication.Liveness analysis says t is dead at its definition, and integer multiplication has no side effect and cannot trap on any mainstream target, so removing the instruction removes nothing observable. Both halves matter: dead plus effect-free. Dead alone is not enough.
a * b computed twice on the same path→Compute it once and reuse the value.The second occurrence is dominated by the first, and neither a nor b was redefined on any path between them, so available-expressions analysis proves the value already exists. What is paid is register pressure: the value has to stay live across the gap, and if that forces a spill the rewrite loses.
List<T> or a generic function→Either one shared implementation with the type erased, or one specialized copy per instantiating type.Erasure keeps one body and represents every T uniformly, which costs boxing and loses the type at run time. Monomorphization emits a body per type, which allows the fields to be laid out inline and the calls to be direct, and pays in code size and compile time. The choice is a language decision made once, not a per-call optimization.
obj.method() where method is virtual→Load the slot from the object’s dispatch table and call through it — unless the receiver type can be pinned down.Indirect calls block inlining, because the callee is unknown at compile time. If class-hierarchy analysis or a profile shows exactly one possible target, the compiler can replace the indirect call with a direct one, or with a type check guarding a direct call. The guard is what makes it legal when the proof is only probabilistic.
a closure that captures a local x→x can outlive the frame. Move it into a heap environment and pass a pointer.A stack slot dies when the function returns, so any captured variable that can escape has to live somewhere else. Closure conversion rewrites the function to take its environment as an explicit extra argument. If escape analysis proves the closure never leaves the frame, the environment can stay on the stack and the allocation disappears.
async fn with several awaits→This is not a function. Rewrite the body as a state machine with one resume point per await.Suspending mid-body is not something a native call frame can do, so the compiler turns every live-across-suspend local into a field of a generated state object and the body into a switch over a state tag. The observable consequence is that stack traces and lifetimes stop matching the source shape.
match on an enum with several arms→Build a decision tree over the discriminant, and check at compile time that the arms cover it.Naive lowering tests the arms in order, which repeats the same discriminant load and comparison. A decision tree tests each column once and branches, so no value is examined twice. The compile-time exhaustiveness check is a separate piece of work that only algebraic data types with a closed set of cases make possible.
a function body with two dozen named temporaries→Names are free. What costs is how many of them are live at the same instant.The IR has unbounded virtual registers, so the number of names says nothing. The allocator builds live ranges and asks how many overlap at each program point; twenty names used one after another need one register, while four names live across the same call need four.
more values live at one point than there are registers→Something has to go to memory. Pick the cheapest victim and spill it.The interference graph cannot be coloured with the available number of colours, so the allocator selects a value to keep in a stack slot, inserting a store after its definition and a load before each use. A spill inside a loop costs a memory access per iteration, which is why spill-cost heuristics weight uses by loop depth.
if (c) { x = 1; } else { x = 2; } print(x);→Two definitions reach one use. Insert a phi at the merge.SSA requires exactly one definition per name, and a merge block is reachable from both branches, so neither definition alone can name the value. The phi is a notation for "whichever predecessor we came from", not a machine instruction; leaving SSA replaces it with copies on the incoming edges.
a call into a function defined in another source file→I cannot see the body. Assume it reads and writes anything reachable, and do not inline it.Separate compilation means the callee is a symbol, not code. Every fact about memory that alias analysis had established is invalidated across the call, and values in caller-saved registers must be spilled around it. Link-time optimization defers code generation so the linker can see both bodies and undo exactly this pessimism.
a call site that has seen one receiver type ten thousand times→Assume it stays that type, compile the specialized version, and guard the assumption.A JIT has a fact a static compiler cannot have: what actually happened. It emits a cheap check of the receiver shape and, on the fast path, code specialized to that shape. If the check ever fails it deoptimizes back to the interpreter, which is why the assumption can be aggressive without being wrong.
a loop that the profile says is hot→Spend the optimization budget here: hoist what does not change, unroll, try to vectorize.Optimization is a budget allocation problem, and a loop body multiplies every saved instruction by its trip count. Hoisting needs the computation to be invariant and safe to execute even if the loop runs zero times; vectorization needs the iterations to be independent, which usually means proving that the memory accesses do not overlap.
x / 0 with a literal zero divisor→Do not fold this. Emit the division.Folding evaluates the expression during compilation, and this expression faults. Moving a run-time fault to compile time changes an observable behavior: the program might never have reached that line. Compilers therefore refuse to fold any operation that can trap, which is the same rule that keeps a trapping load from being hoisted out of a branch.
signed integer arithmetic in C that could overflow→Overflow is undefined, so I may assume it does not happen — and simplify accordingly.Undefined behavior is a licence to assume, not a promise to diagnose. From "i + 1 > i is always true" a compiler concludes a loop is finite, or that an index fits in a wider register, and deletes the check you wrote to detect the overflow. Unsigned arithmetic wraps by definition, so the same assumption is not available there.
a pointer parameter marked restrict or noalias→Nothing else reaches this memory. I may keep loads in registers and reorder stores.Without an aliasing guarantee, a store through one pointer might change what a load through another pointer sees, so the compiler must reload after every store. The annotation is a promise from the programmer, not a proof: if two restrict pointers do alias, the resulting behavior is undefined and the miscompilation is the programmer’s.
#include <header>→Copy this file in, textually, right here — in every translation unit that names it.The preprocessor runs before the language does, so the same declarations are parsed once per translation unit. That is why a widely included header dominates build times, and why the same header seen with different macros defined can produce object files that disagree about a type’s layout.
constexpr, comptime or a const-evaluable function→Run this during compilation and put the answer in the binary.A compile-time evaluator is an interpreter living inside the compiler, restricted to the subset of the language with no observable effects. What is paid is compile time and, in the languages that allow deep recursion here, memory: the evaluation happens once per instantiation rather than once per run.
arr[i] in a memory-safe language→Emit a bounds check — then try very hard to prove it redundant.Safety is a language guarantee, so the check is mandatory unless the compiler can prove it never fires. Inside a loop with a known trip count and a monotone index, range analysis often establishes exactly that and the check is removed. When it cannot, the branch stays and is usually well predicted, which is why the cost is smaller than the instruction count suggests.
try { ... } catch { ... }→Nothing on the path where no exception is thrown. Emit tables the runtime reads when one is.Mainstream implementations use table-driven unwinding: the compiler records, per code range, which cleanups to run and which handler applies, and stores it in a separate section. Entering a try block costs no instructions; throwing costs a table lookup per frame, which is why exceptions are cheap until they are used for control flow.
a variable declared volatile→Every read and every write is observable. No caching in a register, no reordering, no elimination.The qualifier moves the accesses into the set of behaviors the as-if rule must preserve, which disables the analyses that would otherwise remove or merge them. It says nothing about atomicity or about ordering with respect to other threads, which is the misuse that makes it a poor substitute for an atomic.
the debugger says a local is <optimized out>→That value had no storage at this instruction.The variable lived in a register, its last use was earlier, and the allocator handed the register to something else. The debug info records where a value lives per program range, so past the end of the range there is nowhere honest to point. It is a truthful report about the compiled code, not a missing feature.
a directly recursive function→Inlining this does not terminate. Stop at a depth bound, or do not start.Inlining substitutes the body at the call site, and a recursive body contains the call again. Implementations cap recursive inlining at a small depth, or handle only the self-tail-call case by turning it into a jump. This is one of the clearest places where the limit is structural rather than a heuristic about size.
a C++ template used with three different types→Three separate functions, three separate mangled symbols.A template is a pattern, not code; instantiation produces a distinct definition per argument list. The name mangling encodes the argument types so the linker can tell the instantiations apart, and so that duplicates emitted in several translation units can be folded rather than reported as multiple definitions.
if (false) { ... }→That block is unreachable. Remove it and everything only it referenced.Constant propagation replaces the condition with a known value, the CFG builder drops the edge, and the block has no predecessors left. Elimination then cascades: values defined only there become dead, and functions called only there may become unreferenced. This is why dead-code elimination runs repeatedly rather than once.
a && b→Not an arithmetic operator. A branch.Short-circuit evaluation means b is evaluated only when a is true, which is a control-flow property and cannot be expressed by an instruction that takes both operands. The front end lowers it to a conditional jump and a merge, which is exactly why writing a guard as a && a->next works and writing it as a & (a->next != 0) does not.
extern "C" on a declaration→Do not mangle this name, and use the platform C calling convention.C++ encodes parameter types into symbol names so that overloads can coexist; C does not. The annotation opts a declaration out of both the mangling and the C++ specific parts of the ABI, which is what makes a symbol findable from another language’s linker and from a dynamic-symbol lookup.
building with -O0 instead of -O2→Give every variable a home in memory and keep the instruction order matching the source.A debug build is not an unoptimized build by accident; it is optimized for a different objective. Keeping every local in a stack slot means the debugger can always find it and you can assign to it from the prompt, at the cost of a load and a store around every use.
import or a module declaration→Read the interface, not the implementation, and rebuild only what the interface change touched.An interface file gives the compiler the declarations it needs without the bodies, so a change confined to a body does not invalidate the modules that depend on it. That separation is what makes an incremental build proportional to the edit; a textual include has no such boundary, so any header edit invalidates everything downstream.
a + b in Python→Emit one bytecode instruction now and decide what addition means when it runs.CPython compiles the module to bytecode before executing any of it, but the operand types are not known until the instruction executes, so the instruction dispatches on the objects it finds. That is why the arithmetic is slow and why the same source is fast under an implementation that specializes the instruction after observing the types.
x: number in TypeScript→Check it, then delete it. Nothing about it reaches the output.The compiler is a type checker followed by a syntax downleveller; the emitted JavaScript contains no annotation and performs no check. Every type error is a build-time diagnostic and every value crossing an API boundary at run time is unvalidated, which is why parsing external data still needs a run-time schema.
&mut x in Rust→This reference is the only way to reach that memory right now.The borrow checker enforces uniqueness at compile time, so the compiler can attach a noalias attribute and the middle end may keep loads in registers across stores through other pointers. The safety rule and the optimization licence are the same fact stated twice, which is why weakening one weakens the other.
an object allocated and never stored anywhere lasting→Nothing outside this frame can reach it. Put it on the stack, or replace it with its fields.Escape analysis asks whether a reference can be reached after the frame returns. If it cannot, the heap allocation is unnecessary and the object can become a set of virtual registers, which then participate in every other scalar optimization. A single store into a global or an argument passed to an opaque function defeats the whole analysis.