Skip to content
academia.sh

Lesson 13 / 16

Compiler, Interpreter, and Virtual Machine

Three models for executing source code, the trade-offs between them, and the distinction between a language and its implementation.

Contents

The previous topic established that the processor executes bit patterns in memory as instructions. A programmer does not write bit patterns; a programmer writes readable text. A program closes the gap between the two.

This lesson asks: how does the line result = a + b turn into instructions the processor can execute? This lesson defines three models that differ in when the conversion happens; the following lessons take up the conversion itself, step by step.

Source Code Is Text

A source file is a sequence of bytes written according to the rules defined in the character encodings lesson. The if, +, or variable names inside it carry no meaning for the computer yet; they are only characters.

There are three ways to turn this text into executable behavior. All three do the same job; they differ in when the translation happens.

Compilation

A compiler translates the entire source text into the target machine’s instructions before execution. The output is a directly executable file.

Consequences:

  • There is no translation cost at run time; the program runs directly at processor speed.
  • Because the compiler sees the whole program, it can optimize extensively.
  • A portion of errors are caught before execution, during compilation.
  • The output is tied to a specific instruction set and operating system; another platform requires recompilation.
  • Every change to the source requires a compilation step before it can run.

Interpretation

An interpreter does not translate the source in advance; it reads constructs in sequence and carries out the corresponding operations itself. Translation continues while the program runs.

Consequences:

  • The same source runs on any platform that has an interpreter for it; no separate compilation is required.
  • The cycle between writing and running is short.
  • Every construct is resolved anew each time it executes; this adds a fixed cost per instruction.
  • Most errors remain invisible until the corresponding line executes.

This explains why the cache lesson’s calculation was not demonstrated with a timing measurement: when interpretation adds a fixed cost per element, the difference coming from memory behavior is buried under that cost.

Virtual Machine and Bytecode

The third model combines the two: the source is translated not into a real processor’s instruction set, but into that of a designed machine called a virtual machine. This intermediate representation is called bytecode.

Bytecode is compact and cheap to parse compared to source text, and platform-independent compared to machine code. The virtual machine is the program that executes bytecode; writing one virtual machine per platform is less work than writing a separate compiler for each.

The cost of this layer is speed: bytecode executes slower than native instructions. Just-in-time compilation reduces this cost. The virtual machine detects frequently executed sections while running and translates them into native machine code. The translation cost is paid once, and the gain is collected on every repetition.

Just-in-time compilation has two observable consequences. The first is warm-up: a program’s early iterations run slower than later ones, so performance measurements skip them. The second is access to run-time information: where a compiler must guess which branch actually runs, a just-in-time compiler can decide by observing.

Separating a Language from Its Implementation

A common confusion is assuming these models belong to the language itself. The phrases “compiled language” and “interpreted language” are misleading.

A language is defined by its syntax and semantics; how it executes is a decision made by the implementation. The same language can have both a compiler and an interpreter, and in practice both are often written. Languages known for being interpreted have compilers, and languages known for being compiled have interpreters.

The accurate statement is: this implementation of this language translates the source into bytecode and executes it on a virtual machine. This distinction shows which level a claim about a language’s performance actually belongs to.

Who Translates the Translator

A compiler is itself a program, written in some language. This raises a question that looks circular at first: what language is a language’s first compiler written in?

The answer is simple: another language. The new language’s first compiler is written in an existing language and translates a small subset of it. Once that compiler works, the language’s own compiler can be rewritten in the language itself and built with the first one. From then on, the compiler compiles itself; this is called a self-hosting compiler.

The same reasoning applies to interpreters and virtual machines. At the bottom of the chain, there is always a program that already runs on that platform.

The practical consequence: a language’s execution model depends not on its definition but on the history of its ecosystem. A later implementation of the same language can choose an entirely different model.

Comparing the Three Models

Criterion Compilation Interpretation Virtual machine
Translation time Before execution Continuously while running First to bytecode, then while running
Portability Tied to the target platform Anywhere an interpreter exists Anywhere a virtual machine exists
Execution speed Highest Lowest In between; increases with just-in-time compilation
Feedback loop Requires a compilation step Shortest Short
Error detection A portion during compilation At run time Between the two

The boundaries are not sharp. A compiled language can have a runtime library; an interpreted language can translate the source into an intermediate representation first. The models are axes that real implementations sit on.

Seeing the Two Stages

Python stands between the second and third of these three models: the source is first translated into bytecode, then executed on a virtual machine. The two stages can be invoked separately:

source = "result = a + b"

code = compile(source, "<example>", "exec")   # first stage: text -> code object
print(type(code))                              # <class 'code'>
print(code.co_names)                           # ('a', 'b', 'result')

environment = {"a": 10, "b": 4}
exec(code, environment)                        # second stage: execution
print(environment["result"])                   # 14

import dis
dis.dis(code)                                  # listing of the generated bytecode

The compile call executes nothing; it only parses the text and produces a code object. Execution is started separately with the exec call. That the two steps can be separated is direct proof that translation is a different task from execution.

The instruction names and count in the dis output vary by the implementation’s version; bytecode is not a standard but an internal detail of the virtual machine. What is stable is the existence of the listing: the source text has become a sequence of instructions before execution.

Summary

  • Source code is text; it must be translated into machine instructions before it can execute.
  • A compiler translates before execution: it provides speed and compile-time error detection, and the output is tied to a platform.
  • An interpreter translates while running: it provides portability and a short feedback loop, and adds a cost per instruction.
  • The virtual machine model translates the source into platform-independent bytecode; just-in-time compilation closes the gap by translating frequently executed sections into native code.
  • Being compiled or interpreted is a property of the implementation, not the language.
  • A language’s first translator is written in another language; it can later transition to a translator that compiles itself.

Next Step

This lesson addressed when translation happens, not how. The next lesson will open the source text’s path from characters to instructions stage by stage: tokenizing, extracting structure, semantic checking, and code generation.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close