Guide

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.

StrategyExamplesBuild timeRun time
Ahead-of-time (AOT) compiledC, C++, Go, Rust, SwiftParse, check types, optimize, emit a native binaryThe CPU runs the machine code directly
Interpreted (bytecode VM)CPython, Ruby (CRuby), LuaCompile to bytecode, often at import timeA C loop reads and runs one bytecode at a time
JIT-compiledJVM, V8 (JavaScript), .NET, PyPyCompile to bytecodeInterpret 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:

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

Python
def main():
    total = 0
    for i in range(100_000_000):
        total += i
    print(total)

main()
Go
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):

LanguageWhat the runtime providesStartup
C, RustStartup routine, allocator, panic handling~1 ms
GoGoroutine scheduler, garbage collector, netpoller~2 ms
Python (CPython)Interpreter, reference counting, import system~14 ms
Node.jsV8 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.

CppCon 2017: Matt Godbolt “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”CppCon