Machines and programs
Compiled, interpreted and JIT
The three ways a language turns source into machine work, who pays the cost and when, and why the same summing loop runs about 57 times faster in Go than in CPython.
Every language has to turn source text into the bytes a CPU runs. There are three broad strategies, and most real languages mix them. The strategy decides when the cost is paid: at build time, on every operation, or while the program warms up.
Concept
Before you start: machine code is the binary form of instructions for one CPU family, such as x86-64 or ARM64. A bytecode is a compact instruction set for a virtual machine, not for a real chip.
| Strategy | Examples | Build time | Run time |
|---|---|---|---|
| Ahead-of-time (AOT) compiled | C, C++, Go, Rust, Swift | Parse, check types, optimize, emit a native binary | The CPU runs the machine code directly |
| Interpreted (bytecode VM) | CPython, Ruby (CRuby), Lua | Compile to bytecode, often at import time | A C loop reads and runs one bytecode at a time |
| JIT-compiled | JVM, V8 (JavaScript), .NET, PyPy | Compile to bytecode | Interpret first, then compile hot code to machine code while running |
Ahead-of-time compilation
A compiler such as gcc, rustc or the Go toolchain sees the whole program and every type. It can spend minutes optimizing. It inlines small functions, keeps variables in registers, and uses SIMD instructions that process 4 to 16 numbers at once. The cost is paid at build time, and the binary is tied to one CPU family and OS.
Interpretation
CPython compiles your .py file to bytecode and caches it in __pycache__. A C loop, the eval loop, reads one bytecode at a time. You can inspect the bytecode with dis:
import dis
def total_to(n):
total = 0
for i in range(n):
total += i
return total
dis.dis(total_to)The loop body prints as six bytecodes per iteration (output trimmed, Python 3.14):
L1: FOR_ITER 11 (to L2)
STORE_FAST 2 (i)
LOAD_FAST_BORROW_LOAD_FAST_BORROW 18 (total, i)
BINARY_OP 13 (+=)
STORE_FAST 1 (total)
JUMP_BACKWARD 13 (to L1)Each bytecode costs a dispatch, which is a jump that is hard to predict. BINARY_OP cannot add two machine integers directly. total and i are pointers to heap objects. The interpreter checks their types, adds them, and allocates a new object for the result once the value passes 256.
Walking through it: the same loop in Python and Go
Sum the integers from 0 to 99,999,999 in both languages.
def main():
total = 0
for i in range(100_000_000):
total += i
print(total)
main()package main
import "fmt"
func main() {
total := 0
for i := 0; i < 100_000_000; i++ {
total += i
}
fmt.Println(total)
}Both print 4999999950000000. On an Apple M-series laptop in 2026 (Python 3.14, Go 1.26), the Python loop took 1.43 s and the Go loop took 25 ms:
Python: 1.43 s / 100,000,000 iterations ≈ 14 ns per iteration
Go: 25 ms / 100,000,000 iterations ≈ 0.25 ns per iteration
Ratio: 1.43 s / 0.025 s ≈ 57×At about 4 GHz, 0.25 ns is roughly one cycle. The Python loop spends around 55 to 60 cycles per iteration on dispatch and heap objects. Numbers vary by machine and version, but a 30 to 100× gap on pure-Python arithmetic loops is normal.
Tip
This is why NumPy works. np.arange(100_000_000).sum() runs the loop in compiled C over packed machine integers. Use Python to orchestrate work, and move per-element loops somewhere faster.
JIT compilation
A just-in-time compiler interprets first, so startup is fast. It counts how often each function runs, then compiles the hot ones to machine code while the program runs. It can use facts an AOT compiler never sees, such as which types actually flow through a function. If a type assumption breaks, it deoptimizes back to the interpreter.
The practical result is warm-up. A freshly started JVM or Node process serves its first requests more slowly, because the hot code is not compiled yet.
Runtimes add startup cost
A runtime is the code that runs beside your program, such as a scheduler, a garbage collector or a JIT. Startup time shows the difference (hello-world, 2026 laptop):
| Language | What the runtime provides | Startup |
|---|---|---|
| C, Rust | Startup routine, allocator, panic handling | ~1 ms |
| Go | Goroutine scheduler, garbage collector, netpoller | ~2 ms |
| Python (CPython) | Interpreter, reference counting, import system | ~14 ms |
| Node.js | V8 and libuv (event loop, thread pool) | ~20 ms |
| Java (JDK 21) | Class loader, interpreter, C1/C2 JITs, GC | ~35 ms |
Big applications widen the gap. A Spring Boot service can take 5 to 20 seconds to start. That matters for serverless functions, where every cold start is visible to the user.
Common mistakes
- Comparing a cold JVM against a warm one. Measure after warm-up, with production-sized data.
- Assuming "compiled" means fast everywhere. A compiled binary still pays for allocations, locks and system calls.
- Running per-element loops in Python over large data. Move the loop into NumPy, a database or a compiled extension.
Check yourself
- Why is the Python loop slower even though both loops do the same arithmetic? Each iteration dispatches six bytecodes and manipulates heap-allocated integer objects.
- Why might the first minute after a JVM deploy have worse latency? The JIT has not yet compiled the hot code, so early requests run in the interpreter.
Key takeaways
- AOT compilers (C, Go, Rust) pay at build time and produce native code.
- Interpreters (CPython) pay on every operation. JITs (JVM, V8) start interpreted and compile hot code as they run.
- The same summing loop ran about 57× faster in Go than in CPython: about 0.25 ns versus 14 ns per iteration.
- Runtime choice sets startup time, which matters for serverless and autoscaling.
Further learning
Next, see why unpredictable branches make some loops slow, in branch prediction.
Matt Godbolt, who created Compiler Explorer, walks through what compilers do to your code. Check any "the compiler will optimize that" claim with it.
Related
The CPU loop
What a core does billions of times a second, why pipelining and out-of-order execution exist, and what instructions per cycle (IPC) tells you about your code.
Branch prediction
Why a CPU guesses which way every if and loop goes, what a wrong guess costs, and why sorting an array can make an unrelated loop several times faster.
How code runs
From source file to running instructions. Covers the CPU's loop, compilers, interpreters and JITs, branch prediction and the system call boundary.