Skip to content

Commit e858d0b

Browse files
authored
Merge pull request #145 from thisisthepy/feat/tp-compiler-comparison
Docs: Compare TypedPython with Cython, numba and LPython at design level
2 parents 6de021a + aac43f0 commit e858d0b

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# TypedPython against Cython, numba and LPython (design-level comparison)
2+
3+
Requested by the user (2026-10-04, via the leader): compare the current TypedPython design with
4+
Cython, numba and LPython **logically first**; measurements come later (the harness is on
5+
`feat/bench-cython-numba`, not yet measured). Issues: #41, #125.
6+
7+
Evidence tags: **[doc]** stated in the tool's own documentation (links at the end), **[src]** read in
8+
this repository's code, **[measured]** a number from our own runs (2026-10-03 quiet window, develop
9+
e888e7f5, macOS arm64), **[inferred]** reasoning, not verified.
10+
11+
## Conclusion
12+
13+
**Where TypedPython wins**
14+
15+
1. **Deployment.** It is ahead-of-time C that becomes a CPython extension, built with the target's own C compiler. So it runs on iOS, on Android, and next to a GraalVM native image. [src] [doc]
16+
- numba compiles at run time through LLVM, and its AOT path (`numba.pycc`) is deprecated with no replacement yet [doc]. It needs LLVM on the device, which rules out iOS. [inferred]
17+
- LPython AOT produces standalone binaries, and its `@lpython` JIT needs a C compiler at run time [doc]. Neither fits an app that embeds CPython. [inferred]
18+
- Cython has the same deployment model as TypedPython. [doc]
19+
2. **CPython identity by default.** The others give up semantics either by default or once you type the code.
20+
- What TypedPython keeps:
21+
- int overflow promotes to a big int, via deopt [src];
22+
- bounds checks stay unless proven [src];
23+
- exceptions are CPython's own [src];
24+
- signals and GIL hand-off work since #141 phase 1 [src].
25+
- numba: integers are fixed width and wrap; bounds checks are off by default; globals are frozen at compile time. [doc]
26+
- Cython: typed C integers wrap unless `overflowcheck=True` (off by default). [doc]
27+
- LPython: requires its own fixed-width types (`i32`, `f64`). [doc]
28+
3. **No new syntax, gradual by function.**
29+
- TypedPython takes types from ordinary annotations plus Pyrefly's inference. A function it cannot prove stays interpreted, with a reason. [src]
30+
- Cython needs `cdef` or `cython.*` types to leave the object path. [doc]
31+
- LPython needs its own type names. [doc]
32+
- numba needs no annotations, but rejects code outside its subset with a `TypingError`. [doc]
33+
4. **Kotlin interop inside compiled code.** No other tool has a counterpart. [src]
34+
35+
**Where TypedPython loses, and the design item that would close each gap**
36+
37+
| Gap | Who wins it | Closing design item | Identity cost |
38+
|---|---|---|---|
39+
| Per-call overhead on small leaf functions (spectral-norm's `eval_a`): `tp_enter_call`/`tp_leave_call` on every call, probably no inlining | numba and Cython inline small functions [inferred] | Depth precheck plus a fast entry for callees outside every cycle, with a fallback to the per-call path (#41 plan), then let the C compiler inline | none |
40+
| Checked i64 arithmetic in hot integer code | numba, Cython typed, LPython (unchecked) | Interval proofs that drop provably safe checks (for example `i + j` with both below `len(list)`), and a shift for a floor division by a power of two of a proven non-negative value | none |
41+
| Lists are copied in and written back, O(n) per call | numba (numpy arrays) and Cython (memoryviews) are zero-copy [doc] | Accept buffer-protocol arrays (`array.array`, numpy, memoryview) as zero-copy `ArrayParam`. Since no user code runs while the array is live (the existing contract), direct access is observably the same | none |
42+
| Strings and dicts (wordfreq is 1.1x [measured]) | numba `typed.Dict` [doc], Cython C-level code [inferred] | An exact-type `dict[str, int]` and `str` fast path that calls the same C API (`PyDict_GetItem`/`SetItem` on exact `dict` and exact `str` keys, so no user `__hash__` or `__eq__` runs) | none |
43+
| Object allocation (binary-trees is 1.7x [measured]) | Cython `@cython.freelist` [doc]; numba keeps objects out of CPython's GC [doc] | N-12 virtual objects (#143) | **GC timing**, a user decision |
44+
| Float reductions are not vectorised | numba `fastmath=True`, Cython `-ffast-math` (both opt-in) [doc] | None that keeps identity: reassociating float sums changes results. Integer reductions and element-wise loops still vectorise. An opt-in `fastmath` would break identity, so it is a user decision | **yes**, if offered |
45+
| Parallel loops | numba `parallel=True`/`prange`, Cython `prange` with OpenMP [doc] | After free-threaded support: parallel loops whose result is order-independent (element-wise), or reductions in a fixed order | none if restricted that way |
46+
47+
Defaults are on the same footing: numba's `fastmath`, `parallel` and `nogil` are all off by default
48+
[doc], and Cython's default directives keep Python semantics (`boundscheck=True`, `wraparound=True`,
49+
`cdivision=False`) [doc]. Most of the "they are faster" headline numbers come from opting out of
50+
semantics.
51+
52+
## 1. Where type information comes from, and what happens when it is missing
53+
54+
| | TypedPython | Cython | numba | LPython |
55+
|---|---|---|---|---|
56+
| Source | Ordinary annotations plus Pyrefly's static inference [src] | `cdef`/`cython.*` declarations; in pure mode, `float` becomes a C double while `int` stays a Python object [doc] | Specialised at run time from argument types, with inference over the bytecode [doc] | Mandatory annotations in its own types (`i32`, `f64`, `list[i32]`) [doc] |
57+
| When it cannot type something | The function stays interpreted, with a diagnostic; a guard failure at run time deopts that one call [src] | Compiles anyway on the generic object path (slow, but correct) [doc] | `TypingError`: the function does not compile [doc] | Compile error [doc] |
58+
| Granularity | Per function, and per call when deopting [src] | Per variable | Per function, per argument signature | Per program |
59+
60+
## 2. Code generation path and when compilation happens
61+
62+
| | Path | When | Mobile / GraalVM-embedded CPython |
63+
|---|---|---|---|
64+
| TypedPython | Typed IR, then C, then the target's C compiler, giving a CPython extension module [src] | AOT, incremental per module [src] | Yes: NDK, Xcode, desktop clang [src] |
65+
| Cython | C, then a C compiler, giving an extension module [doc] | AOT | Yes (Kivy and BeeWare ship Cython extensions) [inferred] |
66+
| numba | LLVM IR, JIT through llvmlite [doc] | At the first call per signature; `cache=True` stores it on disk [doc]; `numba.pycc` AOT is deprecated [doc] | No on iOS (JIT, LLVM on the device) [inferred] |
67+
| LPython | ASR, then LLVM, C, C++, WASM or x86 [doc] | AOT standalone binaries, or the `@lpython` JIT (C backend only at present) [doc] | Not as part of an embedded CPython [inferred] |
68+
69+
## 3. Object model
70+
71+
- **TypedPython** [src]
72+
- Unboxing: scalars (`int` as i64, `float` as double, `bool`) are unboxed.
73+
- Arrays: `list[float]` and `list[int]` parameters become native arrays through copy-in and write-back, and local arrays are native.
74+
- Classes: `__slots__` classes (N-11) are real objects with direct slot access.
75+
- Everything else is an opaque object.
76+
- Refcounting: every object operation does a real incref or decref.
77+
- Not covered: no dict or str fast path.
78+
- **Cython**
79+
- Unboxing: C types where declared. [doc]
80+
- Classes: `cdef class` objects are real CPython objects with C fields. [doc]
81+
- Arrays: memoryviews give zero-copy access to buffers. [doc]
82+
- Refcounting: Cython's own code generation does it. [doc]
83+
- Lists and dicts go through the C API. [inferred]
84+
- **numba**
85+
- Its data lives outside CPython: arrays are managed by NRT, and `typed.List` and `typed.Dict` are numba's own types. [doc]
86+
- Classes: `jitclass` is experimental. [doc]
87+
- Lists: reflected Python lists are pending deprecation. [doc]
88+
- Interop: Python objects appear only at the boundary or in `objmode`. [doc]
89+
- **LPython**
90+
- Its own value types, with lists and dicts of fixed element types. [doc]
91+
- Interop: CPython objects are reached through `@pythoncall`. [doc]
92+
93+
## 4. CPython identity
94+
95+
| | TypedPython | Cython (typed) | numba | LPython |
96+
|---|---|---|---|---|
97+
| Integer overflow | Promotes to a big int (deopt) [src] | Wraps; `overflowcheck` is off by default [doc] | Fixed width, wraps [doc] | Fixed width (`i32`/`i64`) [doc] |
98+
| Bounds | Checked unless proven [src] | `boundscheck=True` by default [doc] | Off by default [doc] | Not documented [doc gap] |
99+
| Division by zero | CPython's `ZeroDivisionError` [src] | `cdivision=False` by default [doc] | `error_model='python'` by default [doc] | Not documented |
100+
| Globals | Read live, or through an entry snapshot plus a guard (closed functions) [src] | Live | **Frozen at compile time** [doc] | Not applicable |
101+
| Signals, other threads | Polled since #141 phase 1; deferred while a snapshot is live [src] | Not polled unless the code calls `PyErr_CheckSignals` [inferred] | Not polled; `nogil=True` releases the GIL [doc] | Not applicable |
102+
| Trace and profile | No events (#142) [src] | `linetrace`/`profile` directives, off by default [doc] | None [inferred] | Not applicable |
103+
| GC timing | Unchanged for real objects; N-12 would change it [src] | Unchanged [inferred] | Its data is not GC-tracked [inferred] | Not applicable |
104+
| Exceptions | CPython's own; arrays write back partial updates [src] | CPython's own | Limited (constant arguments); memory allocated in a function that raises leaks [doc] | Not documented |
105+
106+
## 5. Loops and arrays
107+
108+
- Bounds-check elimination:
109+
- TypedPython uses interval proofs that the verifier checks again (`proven` ops). [src]
110+
- Cython and numba drop all bounds checks when told to (`boundscheck=False` or the default). [doc]
111+
- Vectorisation:
112+
- All four leave it to the backend (clang or LLVM). [inferred]
113+
- TypedPython builds with `-ffp-contract=off -fno-fast-math` for bit-identical floats, so float reductions do not vectorise, and neither do they under numba's or Cython's defaults. [src] [inferred]
114+
- Parallelism:
115+
- numba and Cython have `prange`. [doc]
116+
- TypedPython has none yet; it needs the free-threaded build first (AGENTS.md §12.8).
117+
118+
## 6. Call boundary cost
119+
120+
- **Python → compiled code**
121+
- TypedPython: a vectorcall wrapper checks argument types exactly and copies arrays in. The copy is O(n) per call, which is a real cost for many small calls on large lists. [src]
122+
- Cython: a C call, plus unboxing, plus buffer acquisition for memoryviews. [inferred]
123+
- numba: a dispatcher that typechecks the arguments against the compiled signatures. [doc] [inferred]
124+
- **compiled code → Python**
125+
- TypedPython and Cython: a C API vectorcall. [src] [inferred]
126+
- numba: only through `objmode` blocks. [doc]
127+
- LPython: through `@pythoncall`. [doc]
128+
- **compiled → compiled**
129+
- TypedPython: a direct C call, but with depth counting on every call. That is the spectral-norm finding. [src]
130+
- Cython: a direct C call. [inferred]
131+
- numba: calls between jitted functions inline through LLVM. [inferred]
132+
133+
## 7. The three numeric benchmarks: theoretical ceilings
134+
135+
The measured numbers are interleaved medians from the 2026-10-03 quiet window, speedup against CPython 3.13 with compiled times. [measured]
136+
137+
| benchmark | TypedPython now | Rust | Node | Ceiling for numba, Cython typed and LPython [inferred] | What TypedPython needs to reach it |
138+
|---|---|---|---|---|---|
139+
| nbody | 31.3x (0.129 s) | 0.059 s | 0.117 s | About C speed, near Rust. Short arrays and float math, no reductions across bodies | Find the remaining 2x to Rust first. Candidates: bounds checks not removed on `pos[i]`-style indexing, no inlining across helper calls, and the call per step. Needs the #125-style profile; not verified |
140+
| spectral-norm | 6.3x (0.518 s) | 0.027 s | 0.068 s | About C speed: `eval_a` inlined, integer arithmetic unchecked, the floor division a shift | (1) the depth precheck and fast entry; (2) inlining of `eval_a`; (3) the interval proof `i + j < 2n`, plus a shift for `// 2` once the value is proven non-negative. The `(i+j)*(i+j+1)` product cannot be proven safe without a bound on `n`, so its check stays, but a predicted branch costs little |
141+
| fannkuch-redux | 23.5x (0.250 s) | 0.249 s | 0.261 s | About C speed | Already at Rust parity [measured]; nothing needed |
142+
143+
## LPython: development state (checked 2026-10-04)
144+
145+
- The README says "currently in alpha stage and under heavy development". [doc]
146+
- Latest release: v0.22.0 (2024-07-08). Last push to `main`: 2025-12-11. 580 open issues. [src: GitHub API]
147+
- It compiles "a subset of Python" with mandatory LPython type annotations. [doc]
148+
- Backends: LLVM, C, C++, WASM, Julia, x86. [doc]
149+
- The `@lpython` JIT supports only the C backend at present. [doc]
150+
- It reaches CPython libraries through `@pythoncall`. [doc]
151+
152+
Assessment [inferred]: the project is active but slow, a long way from a stable 1.0. It is not built to run inside an embedded CPython, so it is not a candidate backend for this repository. The design doc's earlier note (§4.3.3 "upstream activity moved to LFortran") still matches the release history: no release since 2024-07.
153+
154+
## Sources
155+
156+
- numba: [JIT options](https://numba.readthedocs.io/en/stable/reference/jit-compilation.html), [deviations from Python semantics](https://numba.readthedocs.io/en/stable/reference/pysemantics.html), [deprecation notices](https://numba.readthedocs.io/en/stable/reference/deprecation.html)
157+
- Cython: [compiler directives](https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html)
158+
- LPython: [repository README](https://github.com/lcompilers/lpython), [2023 announcement](https://lpython.org/blog/2023/07/lpython-novel-fast-retargetable-python-compiler/), GitHub API for release and push dates
159+
- TypedPython: `python-multiplatform-ksp/src/main/python/typedpython/` (`ir.py`, `cgen.py`, `runtime/tp_runtime.h`, `cbuild.py`), `docs/design/typedpython.md` §4.3

0 commit comments

Comments
 (0)