Skip to content

Commit da88b93

Browse files
dsblankclaude
andcommitted
Measure CPython 3.14's experimental JIT on the benchmark suite
Only the Phase 4 tail-call-flattened loop benefits (~1.2-1.4x), since CPython's tier-2 JIT traces bytecode-level loops (backward jumps), and non-tail recursion (fib, closures, HOF, mutual recursion) never forms one. Unlike PyPy's general tracing JIT, this doesn't stack broadly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0b07370 commit da88b93

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

ChangeLog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
`define-native`. See README-PERFORMANCE.md's Phase 10 for the full
2929
account, including two real bugs found while implementing it. 7 new
3030
pinned regression tests in `tests/test_env_lexaddr_vec.py`.
31+
* Measured CPython 3.14's experimental tier-2 JIT (`PYTHON_JIT=1`,
32+
opt-in and off by default) against the benchmark suite: unlike
33+
PyPy's general tracing JIT above, it only speeds up the one shape
34+
that compiles to a literal Python loop -- the tail-call-flattened
35+
loop from Phase 4 (~1.2-1.4x) -- since it only traces loops with a
36+
backward jump in bytecode, and non-tail recursion (fib, closures,
37+
HOF, mutual recursion) never forms one. No code changes; see
38+
README-PERFORMANCE.md's "Running under CPython's own JIT".
3139

3240
## Release 2.1.6 (Jul 23, 2026)
3341

calysto_scheme/src/README-PERFORMANCE.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

17421814
Measured (not estimated) on this machine, see methodology after the table.

0 commit comments

Comments
 (0)