@@ -1737,6 +1737,78 @@ Caveats:
17371737
17381738---
17391739
1740+ ## Running under CPython's own JIT (` PYTHON_JIT=1 ` )
1741+
1742+ CPython 3.13 added an experimental tier-2 JIT (continued in 3.14); it's
1743+ compiled in but off by default even in builds that support it — confirmed
1744+ here via a conda ` py314 ` env running CPython 3.14.1 (Anaconda build):
1745+ ` sys._jit.is_available() ` → ` True ` , ` sys._jit.is_enabled() ` → ` False `
1746+ until ` PYTHON_JIT=1 ` is set in the environment, at which point it flips to
1747+ ` True ` . No source changes are needed either way — like the PyPy section
1748+ above, this is purely a question of which runtime executes the Python
1749+ source this project's own JIT already generates via ` compile() ` /` exec() ` .
1750+
1751+ ### How CPython's tier-2 JIT differs from PyPy's
1752+
1753+ PyPy's JIT is a general tracing JIT: it watches * any* hot function,
1754+ recursive calls included, and can compile a trace through them. CPython's
1755+ tier-2 JIT is narrower by design — it sits on top of the specializing
1756+ adaptive interpreter and only compiles machine code for loops it detects
1757+ as ** repeated backward jumps in bytecode** . A hot function that recurses
1758+ via ordinary calls (no backward jump in its own bytecode) never triggers
1759+ it, no matter how many times it's called.
1760+
1761+ ### Measured: ` scripts/benchmark.py ` , 5 runs per config, averaged
1762+
1763+ | Benchmark | JIT off | JIT on | ratio |
1764+ | ---| ---| ---| ---|
1765+ | ` fib(30) ` , non-tail recursion | 0.0711s | 0.0707s | 1.00× |
1766+ | ** tail-recursive counting loop, 3M iters** | ** 0.1032s** | ** 0.0805s** | ** 1.28×** |
1767+ | mutual tail-recursion, 50K | 0.7203s | 0.7259s | 0.99× |
1768+ | closures allocated per call, 20000 | 0.8886s | 0.8979s | 0.99× |
1769+ | nested closures, 5000 | 0.2639s | 0.2598s | 1.02× |
1770+ | HOF (parameter called as operator), 5000 | 0.3042s | 0.3057s | 1.00× |
1771+ | ` map ` + closure, 20000 elts | 0.1014s | 0.1061s | 0.96× |
1772+ | stateful loop (forced trampoline), 20000 | 0.5197s | 0.5543s | 0.94× |
1773+
1774+ Only the tail-recursive counting-loop benchmark shows a repeatable win; the
1775+ rest are within this document's normal ~ ±5% run-to-run noise band. Confirmed
1776+ the loop result isn't a fluke by scaling it independently (a standalone
1777+ ` count-to ` tail loop, not the shared benchmark harness):
1778+
1779+ | Iterations | JIT off | JIT on | ratio |
1780+ | ---| ---| ---| ---|
1781+ | 3,000,000 | 0.0969s | 0.0760s | 1.28× |
1782+ | 10,000,000 | 0.3409s | 0.2501s | 1.36× |
1783+ | 30,000,000 | 1.0257s | 0.8306s | 1.24× |
1784+
1785+ ### Why only the tail loop benefits
1786+
1787+ Phase 4 (above) already flattens self-tail-recursive Scheme functions into
1788+ a real Python ` while True: ` loop in the JIT-generated source — that loop's
1789+ backward jump is the * only* shape in this benchmark suite that CPython's
1790+ tier-2 JIT can detect and trace. ` fib ` , mutual recursion, per-call
1791+ closures, HOF, and ` map ` all compile (via this project's own AST-to-Python
1792+ JIT) into ordinary recursive Python function calls with no backward jump
1793+ of their own — nothing for CPython's tracer to grab onto, so its JIT adds
1794+ tracing overhead for no payoff on those shapes (occasionally net negative,
1795+ per the table). The forced-trampoline benchmark also sees no benefit: its
1796+ ` while pc: ` dispatch loop is a real bytecode loop, but the call target
1797+ varies every iteration (megamorphic), which is exactly what a specializing
1798+ JIT can't specialize.
1799+
1800+ ** Bottom line:** unlike PyPy — a full alternate interpreter whose tracing
1801+ JIT stacks a general-purpose second optimization on top of this project's
1802+ JIT — CPython's own experimental JIT only pays off on the one code shape
1803+ in this codebase that happens to compile to a literal loop (Phase 4's
1804+ tail-call flattening), and even there the gain (~ 1.2–1.4×) is far smaller
1805+ than PyPy's (~ 60× on the equivalent tail loop, see above). It's still an
1806+ opt-in, off-by-default feature upstream as of 3.14 — worth re-checking as
1807+ CPython's tier-2 JIT matures and widens what it can trace, but not
1808+ something this project should assume or design around today.
1809+
1810+ ---
1811+
17401812## Potential further improvements
17411813
17421814Measured (not estimated) on this machine, see methodology after the table.
0 commit comments