Computer science fundamentals for working engineers path · Running programs · Topic 6 of 13

Compilers, interpreters and JITs

Not started

A CPU executes only its own machine instructions, so every program written in a higher-level language has to be translated. The choice is when: all at once before the program runs (a compiler), a little at a time while it runs (an interpreter), or a mix that starts by interpreting and then compiles the parts that turn out to matter (a just-in-time compiler). Most real languages use a combination, and the label "compiled language" describes an implementation, not the language.

Why it matters: Start-up time, peak speed, portability and what an error message can tell you all follow from how a language gets from source text to machine instructions.

  1. 1The front end is shared

    Every implementation first lexes source into tokens, parses them into a syntax tree and checks it. The differences come after.

  2. 2Compilers translate ahead of time

    C, C++, Rust and Go produce machine code before the program runs: fast start, full optimisation time, one binary per platform.

  3. 3Interpreters execute directly

    CPython walks bytecode in a loop, one instruction at a time: instant start and easy portability, but each operation costs many machine instructions.

  4. 4JITs compile what is hot

    V8, the JVM and .NET start quickly with an interpreter or a quick compiler, profile the running program, and optimise the hot code using what they observed.

1

How it works

The explanation, with examples you can run.

The common front end

Whatever happens later, an implementation starts the same way:

  1. Lexing turns characters into tokens: let x = a + 1; becomes let, identifier x, =, identifier a, +, number 1, ;.
  2. Parsing builds a tree from the tokens according to the grammar, so that a + 1 * 2 groups as a + (1 * 2).
  3. Analysis resolves names, checks types (in typed languages) and reports errors with line numbers.

Then comes the back end: either generate code for a real CPU, or generate code for a virtual machine.

Ahead-of-time compilers

C, C++, Rust, Go and Swift compile to native machine code before the program ships. The compiler (typically through an intermediate representation, as in LLVM) can spend seconds or minutes optimising: inlining, unrolling loops, vectorising, allocating registers. The program starts instantly at full speed and needs no runtime translator. The costs are slow builds, one binary per CPU and operating system, and optimisations limited to what can be proven from the source, without knowing what the real inputs will be.

Interpreters

An interpreter executes the program without producing machine code for it. Simple ones walk the syntax tree; most real ones first compile to bytecode, a compact instruction set for a virtual machine, and then run a loop that fetches, decodes and executes one bytecode at a time. CPython works this way (the .pyc files are cached bytecode). Interpreters start immediately and run anywhere the interpreter runs, but every bytecode costs dozens of machine instructions of dispatch overhead, which is why pure-Python loops are much slower than the same loop in C.

Just-in-time compilers

A JIT combines the two. V8 (Chrome, Node.js) starts JavaScript in the Ignition bytecode interpreter, compiles warm functions quickly with Sparkplug and Maglev, and sends the hottest to TurboFan, its optimising compiler. HotSpot (Java) and RyuJIT (.NET) follow the same tiered idea, as the Java and .NET paths explain.

The advantage of waiting is information. While the program runs, the JIT records which types actually appear and which branches are taken, then compiles code specialised for that behaviour, guarded by cheap checks. If an assumption breaks (a function that has only ever seen integers receives a string), the runtime deoptimises back to the interpreter and may compile again later. The disadvantages are warm-up time, memory for the compiler and its profiles, and less predictable performance.

Choosing in practice

  • Start-up time matters most (command-line tools, serverless functions): prefer ahead-of-time output, such as Go or Rust binaries, GraalVM native-image for Java, or .NET Native AOT.
  • Long-running services: a JIT's warm-up is paid once and its profile-driven optimisations keep paying.
  • Benchmarks of JIT-compiled code must warm up first, or they measure the interpreter.

WebAssembly shows that the line is blurry: it is a portable bytecode that browsers compile to machine code, sometimes ahead of time and sometimes in tiers, just like a JIT.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Why can a JIT sometimes produce faster code than an ahead-of-time compiler for the same source?
  2. 2.Python compiles .py files to .pyc. Does that make CPython a compiler to machine code?
  3. 3.What is the main cost of a JIT compared with ahead-of-time compilation?

0 of 3 right so far. Get all three to finish this topic.