Pipelineimplementation

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.

The question

Is my language compiled or interpreted, and why does nobody give me a straight answer?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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

typicalThe one-to-two-orders-of-magnitude figure is a range observed across tree-walking interpreters for dynamically typed languages on arithmetic-heavy loops; it is not a law. A tree-walker over a statically typed language with unboxed values does much better, and an interpreted loop dominated by a single call into optimized native code — a matrix multiply, a regex match — shows almost no difference at all, which is exactly why numerical Python is viable.

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]].

A tree-walking interpretertypical
  1. Sourcerun time
    Text, held in memory by the running interpreter.
  2. Parserun time
    An AST, built at start-up or per top-level statement.
    Structure. Usually no separate type-checking phase at all.
  3. Walkrun time
    The 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]].

Ahead-of-time compilation to a native executabletypical
  1. Sourceyou write it
    Text on a build machine.
  2. Compilebuild time
    Tokens, 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.
  3. Object filesbuild time
    Encoded machine code plus a symbol table and relocations.
    An artifact independent of the compiler.
  4. Linkbuild time
    One executable image with addresses resolved.
    Cross-module references bound to actual definitions.
    The translation-unit boundary.
  5. Load and runrun time
    Mapped 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

implementationTier counts and thresholds are per-implementation and per-version. HotSpot has an interpreter plus two compilers; V8 has an interpreter and a tiering ladder that has been restructured more than once; CPython added a specialising adaptive interpreter in 3.11 and an experimental JIT later, so a claim about "CPython does not JIT" now needs a version attached.

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]].

Bytecode compiler plus a virtual machine with a JITimplementation
  1. Sourceyou write it
    Text.
  2. Bytecode compilebuild time
    A 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.
  3. Interpretrun time
    Bytecode being dispatched, with profiling counters incrementing.
    Observation: which methods are hot, which types actually occur, which branches are actually taken.
  4. JIT compilerun time
    Machine 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.
  5. Run optimizedrun time
    Machine 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]].

Three questions, six implementationsimplementation
ImplementationWhat exists when the program startsWhen was it producedWhat still translates at run time
Clang, release buildMachine code in an executableBuild time, on another machineNothing
CPython 3.12Bytecode, cached in __pycache__First import, then reusedBytecode specialisation as instructions warm up
HotSpot JVMClass files containing bytecodeBuild timeInterpreted, then compiled per method as it gets hot, and recompiled if a speculation fails
V8JavaScript source textNothing was produced ahead of timeParsing, bytecode generation and optimizing compilation, all during the run
Go toolchainA statically linked executable including its runtimeBuild timeNothing, though a garbage collector and scheduler run alongside
tsc plus NodeJavaScript source, with the types erasedBuild time for the erasure; nothing for executionEverything 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 eval or 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, and ls __pycache__ to see the cached bytecode it did not re-produce.
  • JVM tiering: java -XX:+PrintCompilation prints each method as it is compiled and each time it is deoptimized; the made not entrant lines are failed speculations.
  • V8's decisions: node --trace-opt --trace-deopt, and node --print-bytecode for the pre-JIT representation.
  • A native binary's independence from its compiler: ldd ./program shows what it still needs at run time, and strings on it shows how little of your source survived.
  • C++ under an interpreter, to break the binary framing directly: cling accepts 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.

A language is either compiled or interpreted.
Neither word describes a language. Both describe what one implementation does at one moment, and mainstream implementations do both at once.
Interpreters do not have compilers in them.
Almost every production interpreter compiles first — to bytecode, to a threaded-code representation, or to closures. Pure AST walking survives mainly in teaching implementations and in eval.
If it produces a binary, it is compiled; if it needs a runtime installed, it is interpreted.
Go binaries include a garbage collector and a scheduler; GraalVM produces a single native binary for a JVM language; CPython can be embedded into a native executable. The artifact shape and the translation strategy are independent axes.

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.

implementationEvery claim about CPython, HotSpot, V8, PyPy and LuaJIT is a claim about a version. CPython gained an adaptive specialising interpreter in 3.11 and an experimental copy-and-patch JIT in 3.13; V8 has reorganised its tiers repeatedly. Pin the version before repeating any of it.
specNo mainstream language definition requires a particular implementation shape. The JVM specification is the notable partial exception: it specifies the class file format and the bytecode semantics, but explicitly leaves interpretation, JIT compilation and ahead-of-time compilation all conforming.
typicalThe performance ratios quoted between shapes describe compute-bound microbenchmarks on scalar code. Workloads dominated by I/O, by native library calls or by allocation behave completely differently, and reversing the ranking is easy to arrange.

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

Domains that do not exist yet
  • Programming Languages & Runtime Internals — What the virtual machine does with bytecode once it has it — object layout, dispatch, collection
    This 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.