Compiler versus Interpreter Is Not a Binary
Three implementation shapes, not two categories — and none of them is a property of a language. The sentence to stop saying is named, dismantled and replaced with a question that has an answer.
Is my language compiled or interpreted, and why does nobody give me a straight answer?
The distinction is about *what representation is left standing when the program runs*, and *when* the translation into it happened. A pure interpreter still holds a typed AST at run time; an ahead-of-time compiler holds only machine code and has exited; a bytecode VM holds bytecode and, once a method is hot, machine code it produced itself. Those are three different answers to "what is the program, right now", and that is the only question the words are really about.
All three shapes are legal implementations of the same language definition, because a definition constrains observable behavior and never mentions translation. An implementation is free to choose any shape provided the outputs, the ordering of side effects and the defined error behavior match. Where the definition does constrain the shape, it does so explicitly and for a reason — a language with eval requires the ability to turn a string into executable behavior at run time, so a purely ahead-of-time implementation must either ship a translator with the program or refuse to implement the feature.
Key points
- Compiled and interpreted are properties of an implementation at a moment, not of a language.
- There are at least three shapes, not two: interpret directly, translate fully ahead, or translate partly ahead and finish while running.
- CPython contains a complete compiler and emits bytecode; C++ has interpreters in production use. Both halves of the famous sentence are false.
- The three answerable questions are: what representation exists at start-up, when was it produced, and what still translates during the run.
- The third shape can perform optimizations the second cannot, because it knows what actually happened — and it makes them safe with guards and deoptimization rather than with proof.
The sentence to stop saying
"Python is interpreted, C++ is compiled." It is the most repeated sentence in this subject and it is wrong in both halves, in ways that matter for real engineering decisions.
CPython compiles. It lexes, parses, builds an AST, performs a symbol-table pass, emits bytecode for a stack machine, and caches that bytecode in __pycache__ so it does not have to do it again. There is a compiler in there with all the phases from [[compiler-phases]], and python -m dis will print its output. What CPython does not do is emit machine code — its bytecode is executed by a dispatch loop rather than by the CPU.
And C++ has interpreters. Cling, and the C++ interpreter inside ROOT that it grew out of, are ordinary tools used by physicists daily. It is also compiled at run time, by every C++ program that ships an embedded JIT. Meanwhile the same C++ source can be compiled ahead of time, compiled to WebAssembly and then compiled again by the browser, or type-checked and thrown away by a static analyser that never emits anything at all.
The correct question is not "which is it" but three separate questions with three separate answers: what representation exists when the program starts, when was the translation into it performed, and what still translates while the program runs. Ask those and every real system answers cleanly.
Shape one: translate as you execute
The purest interpreter walks a data structure and performs the effects it describes. There is no separate output artifact; the program is still a tree, or still a string, at the moment it does something. Analysis, if it happens at all, happens immediately before execution and is not preserved.
This is the shape of a shell, of a small tree-walking interpreter, of most template engines and of eval in every language that has it. It is also the shape you should build first when implementing a new language, because it gets you to a running language in days and every later shape is defined by reference to it. See [[tree-walk-interpreter]] and [[atlaslang-interpreter]].
- Sourcerun timeText, held in memory by the running interpreter.
- Parserun timeAn AST, built at start-up or per top-level statement.Structure. Usually no separate type-checking phase at all.
- Walkrun timeThe AST, being traversed, with an environment mapping names to values.The actual values. Every node is re-interpreted on every execution, including every iteration of a loop.
Read it asEverything happens at run time and nothing survives the process. The cost is that the interpretive overhead — dispatching on node kind, looking names up in an environment, boxing every value — is paid on every single execution of every node, which is why a hot loop in this shape is typically one to two orders of magnitude slower than the same loop in shape two. The benefit is that there is no build step, source is the only artifact, and eval is free because the machinery is already there.
Shape two: translate ahead, then run the result
The ahead-of-time shape performs every phase before the program is ever started, emits machine code into an object file, links it, and exits. What runs later is the artifact; the compiler is not present and cannot be consulted. Nothing about the program's own source exists at run time, which is why a stack trace needs separately emitted debug information to name a function at all.
This is the shape of a released C, C++, Rust or Go binary. It gives the fastest possible start — the loader maps pages and jumps — and the largest available optimization budget, because the compiler may spend minutes on a program that will run for months. What it cannot do is use any fact that is only true at run time: the actual argument values, the actual types behind an interface, which branch is actually taken. See [[aot-compilation]].
- Sourceyou write itText on a build machine.
- Compilebuild timeTokens, tree, typed tree, IR, machine IR — all inside a process that will exit.Every answer derivable without running the program.Names, types and structure, except what debug metadata preserves.
- Object filesbuild timeEncoded machine code plus a symbol table and relocations.An artifact independent of the compiler.
- Linkbuild timeOne executable image with addresses resolved.Cross-module references bound to actual definitions.The translation-unit boundary.
- Load and runrun timeMapped pages executing on the CPU. No compiler in the process.The real inputs — which nothing in the pipeline was able to use.
Read it asThe defining property is the gap between the two when columns. Everything on the left happened on a machine that is not this one, possibly years ago, with no knowledge of this run. That gap is what [[profile-guided-optimization]] tries to narrow by carrying a recording of a previous run backwards across it.
Shape three: translate ahead a bit, then again while running
The third shape is the one most widely deployed, and the one the binary vocabulary has no word for. A compiler runs ahead of time but stops at a portable, compact instruction format rather than at machine code. At run time a virtual machine executes that format directly, counts how often each method and loop runs, and hands the hot ones to a second compiler that emits actual machine code — with the enormous advantage of knowing what the types actually turned out to be.
This describes the JVM, the .NET CLR, V8 and JavaScriptCore for JavaScript, PyPy and LuaJIT. The parts are ordinary: a bytecode compiler, a dispatch loop, a profiler and one or more optimizing compilers — see [[bytecode]], [[dispatch-loop]], [[tiered-compilation]] and [[jit-compilation]].
The optimizations available in this shape are genuinely unavailable in shape two. If a call site has only ever seen one receiver type in ten thousand executions, the JIT may inline that implementation directly, guarded by a cheap check that the type is still the one expected. If the check ever fails, the guard traps and execution falls back to the interpreter with the correct state — which is [[deoptimization]], and which is why speculation is safe rather than reckless. See [[guards]] and [[inline-caches]].
- Sourceyou write itText.
- Bytecode compilebuild timeA compact instruction stream for a virtual machine, usually cached on disk.Everything derivable statically, in a form that starts fast and is portable across machines.Source-level structure, but not portability — the artifact still runs anywhere the VM does.
- Interpretrun timeBytecode being dispatched, with profiling counters incrementing.Observation: which methods are hot, which types actually occur, which branches are actually taken.
- JIT compilerun timeMachine code for one hot method, specialised on observed types, with guards.Speculative optimization that no ahead-of-time compiler could justify.Nothing permanently — every speculation is reversible by deoptimizing back to the interpreter.
- Run optimizedrun timeMachine code executing, guards checking, counters still running.Native speed on the code that matters, at the cost of memory for the compiler and its output.
Read it asNotice that the same program is in two representations at once, and moves between them in both directions. That is why "compiled or interpreted" cannot answer the question: the honest answer is "both, at different times, for different parts, and it changes while you watch".
The questions that do have answers
Replace the binary with three questions and every implementation becomes describable, including the awkward ones. A statically typed language whose type checker emits nothing at all, like TypeScript, answers the third question with "nothing" and the second with "at build time, then discarded" — see [[typescript-pipeline]].
| Implementation | What exists when the program starts | When was it produced | What still translates at run time |
|---|---|---|---|
| Clang, release build | Machine code in an executable | Build time, on another machine | Nothing |
| CPython 3.12 | Bytecode, cached in __pycache__ | First import, then reused | Bytecode specialisation as instructions warm up |
| HotSpot JVM | Class files containing bytecode | Build time | Interpreted, then compiled per method as it gets hot, and recompiled if a speculation fails |
| V8 | JavaScript source text | Nothing was produced ahead of time | Parsing, bytecode generation and optimizing compilation, all during the run |
| Go toolchain | A statically linked executable including its runtime | Build time | Nothing, though a garbage collector and scheduler run alongside |
| tsc plus Node | JavaScript source, with the types erased | Build time for the erasure; nothing for execution | Everything V8 does, on the erased output |
How it works
The steps, in the order the compiler takes them.
- Shape one parses to a tree and evaluates the tree, re-interpreting each node on every execution against an environment of name-to-value bindings.
- Shape two runs every phase before execution, emits an object file, links it, and leaves an artifact that runs with no compiler present.
- Shape three emits a compact virtual instruction set, dispatches it in a loop while incrementing counters, and passes methods that cross a threshold to an optimizing compiler.
- The optimizing compiler specialises on observed types and inserts guards that check the assumption still holds.
- A failing guard transfers control back to a lower tier with reconstructed state, so a wrong speculation costs time rather than correctness.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A team rewrites a hot loop in a "compiled language" and sees no improvement, because the original loop spent its time inside an already-native library call and the interpretive overhead was never the bottleneck.
- A benchmark of a JIT-backed runtime reports numbers three times worse than production, because it measured the first few seconds and never left the interpreter — the classic warmup error.
- A deployment mysteriously slows down after a code change that touched nothing hot, because a call site that used to see one receiver type now sees two, the inline cache went polymorphic, and the inlining that depended on it was undone.
- An ahead-of-time build of a language with
evalor reflection fails at run time with a missing symbol, because the code being reached for was never referenced statically and therefore never emitted. - A stack trace from a production native binary shows addresses instead of function names, because the artifact was stripped and nothing at run time knows what anything was called.
When it helps
- Choosing a runtime for a workload: short-lived processes want shape two or a fast-starting shape three; long-lived servers can afford warmup and get most of shape two's performance plus specialisation.
- Interpreting a benchmark. Knowing which shape you are measuring tells you whether the first second is meaningful and whether the numbers will hold at a different input distribution.
- Diagnosing a performance regression that has no corresponding code change, which in shape three is usually a speculation that stopped holding.
When it hurts
- Using the vocabulary at all in a design discussion. "Should this be compiled" has no answer; "can we afford three seconds of warmup on every deploy" has one.
- Assuming shape three always beats shape one. A dispatch-bound workload with no hot method never crosses a compilation threshold and pays the profiling overhead for nothing.
What it costs
Every one of these is paid by something.
- Shape one buys implementation simplicity, no build step and trivial
eval, and pays interpretive overhead on every execution of every node, plus the impossibility of any cross-node optimization. - Shape two buys instant start and the largest optimization budget, and pays a build step, an artifact per target, and permanent blindness to anything only the run knows — plus, for anything dynamic, a static approximation that must be conservative.
- Shape three buys portability, fast start relative to native compilation and speculative optimizations unavailable to shape two, and pays warmup latency, memory for the compiler and its generated code, unpredictable pause behavior when compilation happens, and a far larger implementation to get correct. See
[[jit-costs]].
What else you could do
What a different compiler or language does instead, and when that is better.
- Transpilation: translate to another high-level language and inherit its implementation entirely, which is what TypeScript does and what many languages did to bootstrap. Cheap to build, and the debugging story depends entirely on
[[source-maps]]. - Ahead-of-time compilation of a language usually run under shape three — GraalVM native-image for Java, and .NET Native AOT — trading dynamic loading and reflection for start-up latency and memory. See
[[whole-program-optimization]]. - A bytecode VM with no JIT at all, which is CPython for most of its history: portable, small, predictable, and slower on compute-bound code by a factor that varies with how much time is spent in native libraries.
- Compiling to WebAssembly and letting the host engine finish the job, which puts the seam in a different place again —
[[wasm-model]].
See it for yourself
The flag, dump or tool that shows you this directly.
- Python's compiler output:
python -m dis yourfile.py, andls __pycache__to see the cached bytecode it did not re-produce. - JVM tiering:
java -XX:+PrintCompilationprints each method as it is compiled and each time it is deoptimized; themade not entrantlines are failed speculations. - V8's decisions:
node --trace-opt --trace-deopt, andnode --print-bytecodefor the pre-JIT representation. - A native binary's independence from its compiler:
ldd ./programshows what it still needs at run time, andstringson it shows how little of your source survived. - C++ under an interpreter, to break the binary framing directly:
clingaccepts C++ statements at a prompt and executes them.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Python is interpreted, C++ is compiled." CPython compiles to bytecode and caches it; C++ has interpreters in daily production use. Both halves describe one common implementation and mistake it for the language.
- "Compiled languages are faster." Ahead-of-time compilation to native code usually starts faster and often runs faster, but the comparison is between implementations on a workload, and shape three wins some of those workloads outright by specialising on facts shape two could not have.
- "A JIT is just a compiler that runs late." It is a compiler that runs late *and can be wrong*, because it optimizes on observations rather than proofs. The guard-and-deoptimize machinery is what distinguishes it, and it has no analogue in shape two.
- "Bytecode is machine code for a fake machine, so it is basically the same thing." The instruction set is designed for compactness, portability and easy verification rather than for silicon, which is why stack machines are common in bytecode and rare in hardware —
[[stack-vs-register-vm]].
Misconceptions
The claim, and what is actually true.
eval.Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
Some implementations translate your whole program before running it, some translate as they go, and most mainstream ones do a bit of both: they translate ahead of time into a compact intermediate instruction set, then translate the hot parts into machine code while the program runs. The words compiled and interpreted pick out two points on that line and pretend there is nothing in between.
practical
When someone asks whether to use a compiled language, translate the question. If they mean start-up latency, ask what the process lifetime is — a function invoked per request cannot afford warmup, a server can. If they mean throughput, ask what the code actually does, because native compilation buys nothing on a workload that is already inside a native library. If they mean deployment, ask whether shipping a runtime is a problem. Each of those has an answer; the original question does not.
advanced
The reason the three shapes coexist rather than converging is that each has access to a different set of facts. Ahead-of-time compilation has unlimited time and no knowledge of the run. A JIT has the run and a strict time budget, so it must speculate and be able to undo. An interpreter has both the run and no budget, so it re-derives everything constantly. Systems that appear to beat this trade — profile-guided optimization, tiered ahead-of-time compilation, snapshotting a warmed heap — all work by moving information across the boundary rather than by removing it, and the interesting failure mode of every one of them is a profile or a snapshot that no longer describes what the program now does.
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
- Someone tells you Python is interpreted and Java is compiled. Correct them precisely, without being pedantic about it.
- What can a JIT do that an ahead-of-time compiler cannot, and what does it have to do to make that safe?
- You need to cut cold-start latency on a function that runs for 200ms and is invoked millions of times a day. Which implementation shape helps and what does it cost?
Connections
- Programming Languages & Runtime Internals — What the virtual machine does with bytecode once it has it — object layout, dispatch, collectionThis domain owns the bytecode compiler and the machine code the JIT emits. The VM's own execution machinery is the runtime's half, and the two halves are usually the same source tree, which is why the boundary needs stating.