compiler-category Intermediate 8 min read

AOT vs JIT for Python: An Honest Comparison

Warm-up, peak performance, compatibility and running cost: the AOT vs JIT trade-offs for Python, the cases where each wins, and Pyvorin's position.

Published Sep 1, 2026

Ahead-of-time and just-in-time compilation both replace interpreted bytecode with machine code; they differ on one axis that shapes everything else — when the compilation happens. A JIT compiles while your program runs, after code proves hot. An AOT compiler compiles before your program runs, on a schedule you control. Pyvorin Native 1.0.9 is in the AOT camp, and measured across our 71-workload suite that choice produced a 3.16x geometric mean, a 1.35x median, 54 workloads faster than CPython and 17 slower. Neither strategy is universally better. This page compares them on the dimensions that decide real deployments, and says plainly where each wins.

The two strategies in one paragraph each

A JIT compiler starts your program on the interpreter and watches. When a loop or function accumulates enough executions, the JIT compiles it — using the types and branch behaviour it has actually observed — and swaps the hot path to machine code. Later executions may trigger recompilation with better information. The costs are paid in warm-up time and JIT CPU during the run; the reward is compilation fed by ground truth. PyPy does this across a whole program with a meta-tracing JIT. Numba does it for individual numeric functions, compiling the first time each decorated function is called.

An AOT compiler compiles before execution, from source alone. There is no profiling evidence available, so the compiler must prove what it can statically — types, bounds, shapes — and protect every assumption with a runtime guard that diverts to a safe path if reality disagrees. The costs are paid as a compile step whose time must be amortised, and as a peak-performance ceiling bounded by what static analysis can prove. The rewards are immediate speed from the first call, predictable behaviour, and a compilation bill you can schedule. Pyvorin compiles selected functions this way, locally and in-process, through LLVM.

The trade-offs that matter

DimensionAhead-of-time (Pyvorin)Just-in-time (PyPy, Numba)
When compilation happensBefore execution, on your schedule; cached on disk afterwardsDuring execution, once code proves hot
First-call behaviourMachine code immediately, after a compile step measured in the low hundreds of milliseconds per functionInterpreted until the JIT tier fires; warm-up varies from milliseconds to seconds
Type informationInferred statically; guarded at runtime with deoptimisation on violationObserved from real execution; specialised speculatively
Compile cost visibilityExplicit, reported separately from execution timeBurned inside your process's runtime, often invisible in application timing
Peak performance ceilingHigh on provably typed loops; bounded by static proofHigh on stable workloads with stable types; poor on unstable ones
Compatibility surfacePer function, with a recorded fallback for what it cannot takeWhole-program (PyPy) or decorated subset (Numba)
Failure modeDeclines, marks, falls back to CPython semanticsBails to interpreter; Numba falls back per function

Startup and warm-up: where the strategies genuinely diverge

Warm-up is the argument that decides most AOT-versus-JIT choices, and it is mostly arithmetic. A JIT must recoup its compilation investment from the executions that follow it. A long-running service with a stable hot loop recoups easily — the JIT compiles once and serves a million calls. A batch job that runs for ninety seconds never finishes paying: by the time the JIT has evidence and compiled code, the job is describing its output files. A CLI tool is worse still; it may exit before the JIT has decided anything is hot at all.

AOT inverts the graph. The compile step happens before the clock that matters starts — at deploy time, at import, or on the operator's schedule — and from the first timed call the code runs at its steady-state speed. On our walkthrough host, compiling a representative numeric function cost 346.879 ms, reported separately from execution by the benchmark harness, and the compiled median of 3.999 ms against a CPython median of 34.184 ms amortised that compile step within roughly ten calls. A JIT paying the same compilation cost inside the timed region would need the same ten calls just to break even — and then keeps paying for every recompile triggered by a type that changed.

The honest counterweight: if your process runs for weeks, warm-up is a rounding error and the JIT's ground-truth type information becomes a genuine advantage. A JIT watching real executions can specialise code a static compiler cannot prove safe. On workloads with stable, repetitive, type-stable hot loops, that information advantage shows up in peak numbers.

Peak performance and the information each compiler gets

Strip the marketing from both camps and the ceiling question becomes a contest over whose guesses are better. The JIT guesses from observation — it has seen a million iterations and knows the types. The AOT compiler guesses from proof — it keeps only what it can establish, and guards the rest. Where the types are simple and the loops are regular, static proof and observed reality converge, and both strategies land near the same machine code. That describes most of the suite's winners: numerical computing at a 10.91x category geomean, core integer kernels at 16.24x, object manipulation at 42.28x.

Where they diverge is polymorphism and irregularity. Code whose types genuinely vary at runtime — a function called with ints in one place and floats in another — rewards a JIT's per-call-site specialisation and punishes static proof, which must either generalise or guard. This is why both camps converge on the same pragmatic answer: specialise aggressively, and keep a correct general path for when the assumption breaks. Pyvorin's guards divert to CPython semantics on violation; JITs deoptimise to the interpreter. The machinery differs; the shape of the answer is identical.

Where JITs win

Long-running processes with stable hot loops. Services under continuous load where warm-up cost is spread across days. Code whose performance depends on type behaviour that only shows up at runtime. PyPy's tracing JIT is the strongest general case: whole-program acceleration with no code changes, at the price of warm-up, higher baseline memory, and a compatibility story that differs from CPython in ways that matter for C extensions — Pyvorin vs PyPy covers that comparison honestly, including where PyPy is the better choice. For numeric kernels, Numba's JIT delivers large speedups on decorated functions and remains an excellent tool when its type subset fits your code and its compilation model fits your runtime profile.

CPython's own experimental JIT, shipped in Python 3.13 under PEP 744 and disabled by default, belongs in this column too. It is a copy-and-patch design, included in official macOS and Windows binaries from 3.14, and the project's own release notes describe the performance improvement as modest. It is worth enabling and measuring; it is not yet a reason to change plans.

Where AOT wins

Batch jobs, data pipelines, simulations, scheduled reports — anything where the process is short-lived, cold-start-sensitive, or runs on infrastructure you pay for by the second. The compile step moves off the critical path entirely, first-call speed equals steady-state speed, and the compilation cache means unchanged code pays nothing on subsequent runs. Selective AOT has a second advantage in mixed codebases: because it compiles per function, it can take the numeric core of a program while leaving its parsing, I/O and framework layers on the interpreter untouched, with a recorded fallback guaranteeing the mixed program behaves like CPython everywhere the compiler declined.

The same selectivity is the AOT camp's honest limit. Code the static analysis cannot prove stays slow, full stop, where a JIT might eventually specialise it from observation. Our own suite reports the distribution without flinching: best 202.83x, median 1.35x, worst 0.05x, with string manipulation at 0.85x and parsing at 0.86x sitting slightly below the interpreter. Compilation is not a universal accelerator, and no honest page in either camp should pretend otherwise.

Measuring either strategy fairly

One measurement pitfall belongs to this comparison specifically: warm-up asymmetry makes naive timing favour whichever strategy the harness happens to exercise first. A JIT timed from process start absorbs its compilation into the measured window and looks bad; timed after an unmeasured warm-up period, it looks unbeatable. An AOT compiler timed on first-ever invocation absorbs its compile step; timed on a warm cache, it looks effortless. The defence is the same in both cases — separate the compile cost from the execution cost, report steady-state timing after defined warm-up, and say which is which. Pyvorin's benchmark harness does this by construction: compile time is reported as its own field, distinct from the measured runs. Whatever tool you benchmark, demand the same separation before comparing numbers across the two columns of the table above.

The judgement call

Choose with three questions, answered in order. First, how long the process lives — long enough for a JIT to recoup, or short enough that only pre-paid compilation helps. Second, whether the hot functions have stable, provable types — the terrain where both strategies converge. Third, how much of the codebase is actually hot — enough to justify whole-program JIT machinery, or only a numeric core that selective AOT can take without touching the rest. A nightly risk batch fails the first question for JIT and passes the other two for AOT; a week-long simulation server fails the second for neither. The answers, not the slogans, pick the tool — and where the answers are mixed, the winning architecture is often both: compiled kernels inside an interpreted program, which is exactly the seam Pyvorin is designed to occupy.

Where to go next

Last reviewed 1 September 2026 against Pyvorin Native 1.0.9 and the canonical benchmark artefact dated 13 September 2026 (71 workloads: 54 faster, 17 slower; geomean 3.16x, median 1.35x, best 202.83x, worst 0.05x). The 346.879 ms compile figure is a single-host live capture from the measured walkthrough. CPython JIT facts cite the Python 3.13 and 3.14 release notes and PEP 744.