Compiled vs Interpreted
Languages Explained

Is Python slow because it's interpreted? Is C fast because it's compiled? The reality is more interesting than the simple binary suggests — and understanding it makes you a better programmer regardless of which language you use.

The compiled/interpreted distinction is one of the first conceptual frameworks programmers encounter, and one of the most frequently oversimplified. The real picture — involving ahead-of-time compilation, bytecode, virtual machines, just-in-time compilation, and hybrid approaches — explains why "interpreted languages are slow" is both true in one context and completely false in another.

What compilation means

Compilation is the transformation of source code — the human-readable text you write — into a different form that a machine can execute. The classical form is ahead-of-time (AOT) compilation to native machine code: a compiler reads your entire program, analyses it, optimises it, and produces a binary executable specific to a processor architecture and operating system.

C is the canonical example. When you compile a C program with gcc main.c -o main, the compiler performs lexical analysis, parsing, semantic analysis, optimisation, and code generation. The output (main) is a binary file containing x86 (or ARM, or whatever architecture you targeted) machine instructions that the CPU can execute directly. No translation occurs at runtime; the CPU reads and executes the instructions immediately.

Go follows a similar model. go build main.go produces a statically linked binary with no external dependencies. Go binaries are self-contained — they include the runtime, garbage collector, and all dependencies — and they start fast and run at near-C performance. Go's compilation is significantly faster than C's, which was an explicit design goal.[1]

What interpretation means

An interpreter reads source code and executes it directly, instruction by instruction, without producing a persistent compiled output. The classic mental model: the interpreter is a program that reads your code and simulates what a computer would do if it could understand your language directly.

The traditional critique of interpretation is performance: because the interpreter must parse and dispatch each instruction at runtime rather than having pre-compiled machine code, interpreted programs run slower than compiled ones. This critique has been empirically valid for certain workloads and naively implemented interpreters.

However, essentially no modern widely-used "interpreted" language actually works this way.

The reality: Python and bytecode compilation

Python is commonly described as an interpreted language, but this description is imprecise. CPython (the reference Python implementation) compiles Python source code to bytecode — an intermediate representation — which is then executed by the Python Virtual Machine (PVM), a bytecode interpreter.

The bytecode is stored in .pyc files (in the __pycache__ directory). When you run a Python script for the second time, Python loads the cached bytecode rather than recompiling from source, which is faster. The PVM then executes the bytecode instruction by instruction.

The performance limitation of Python is not primarily that it interprets bytecode. It is that Python is dynamically typed — every operation on every object requires a runtime type lookup to determine what operation to actually perform. Adding two integers requires checking that both are integers, dispatching to the integer addition method, and returning a new Python integer object. In C, adding two integers is a single CPU instruction. This overhead per operation, multiplied across millions of operations in a tight loop, is where Python's performance disadvantage materialises.

JavaScript and JIT compilation

JavaScript's performance story is more dramatic. Early JavaScript engines (pre-2008) were interpreters in the straightforward sense, and JavaScript was regarded as a slow scripting language. The performance requirements of increasingly complex web applications created market pressure for faster engines.

Google's V8 engine (2008) introduced just-in-time (JIT) compilation: the engine monitors code as it executes, identifies frequently executed ("hot") code paths, and compiles those paths to optimised native machine code at runtime. Code that runs in a tight loop — the kind that determines performance — gets compiled to native code and runs at speeds approaching C.

V8's Turbofan optimising compiler, SpiderMonkey's IonMonkey (Firefox), and JavaScriptCore's FTL JIT (Safari) all employ similar techniques. Modern JavaScript benchmarks show performance within 2–5× of equivalent C code for compute-intensive tasks — an extraordinary achievement for a language that is nominally interpreted.[2]

Java and the JVM: compiled and interpreted

Java occupies an explicit middle position. The Java compiler (javac) compiles Java source code to JVM bytecode — like Python, an intermediate form. The JVM then executes the bytecode, but also employs JIT compilation for hot code paths. The result is a language that is "write once, run anywhere" (because bytecode runs on any JVM) but also fast (because JIT compilation optimises frequently executed code to native instructions).

The JVM model has been adopted by Kotlin, Scala, Clojure, and Groovy — all of which compile to JVM bytecode and benefit from the JVM's JIT infrastructure.

Why the distinction still matters

Despite the blurring of the compiled/interpreted boundary, the distinction retains practical significance:

  • Startup time: AOT-compiled languages (C, Go, Rust) start instantly. JVM languages have a startup penalty from JVM initialisation — significant for short-lived CLI tools, negligible for long-running servers. Python and Node.js have intermediate startup times.
  • Predictable latency: JIT compilation introduces occasional compilation pauses and warm-up periods before peak performance is reached. For latency-sensitive systems (trading, real-time audio), AOT compilation offers more predictable performance profiles.
  • Distribution: AOT-compiled binaries (Go, Rust) require no runtime installation. Python scripts require a Python interpreter. JVM programs require a JVM. This affects deployment complexity significantly.
  • Tooling feedback: Compiled languages typically catch type errors and many semantic errors before execution; dynamic interpreted languages surface these errors at runtime. This affects developer experience and testing strategy.

"The language is compiled or interpreted. The implementation is a different question entirely. Python bytecode, JavaScript JIT, and Java's JVM all demonstrate that the boundary is an engineering choice, not a fundamental property."

— Eli Bendersky, Python Internals blog, 2012[3]

References

  1. Pike, R. (2012). Go at Google: Language Design in the Service of Software Engineering. Google. go.dev
  2. Gal, A., Eich, B., Shaver, M., et al. (2009). Trace-based just-in-time type specialization for dynamic languages. ACM SIGPLAN Notices, 44(6), 465–478. doi.org
  3. Shaw, M. (1984). Abstraction techniques in modern programming languages. IEEE Software, 1(4), 10–26. doi.org