Cython strategy¶
Work in progress
This page is a stub. The full thesis arrives in the JSS paper.
The rule¶
We reach for a Cython kernel only when an empirical benchmark says NumPy alone
isn't enough. The cross-language harness in benchmarks/compare/ is the
gatekeeper: every hot path runs in three flavours — pure Python (or NumPy + a
Python loop), Cython-compiled, and Julia — and we Cythonise the path only if the
NumPy column lags Julia by more than the harness's measurement noise floor.
Reasons:
- The Python ecosystem has no shortage of "I assumed I needed Cython, then measured and didn't" stories. We don't want to be one.
- Each Cython kernel adds a compiled extension, which adds a per-platform wheel
to build and ship. Reading the existing
wheels.ymlis one good way to internalise how much that costs. - The paper's central empirical contribution is the classification below, and the classification is only meaningful if every cell in it was measured.
The N=4 operation-shape classification¶
Four kernels are shipped in M1.5. They line up with four structurally distinct
operation shapes:
| Operation shape | Example | Cython-over-NumPy speedup |
|---|---|---|
| Non-vectorisable recurrence | rec_linear (AR(p)) |
~65× |
| Outer loop of per-group C reductions | fconvert_*_aggregate |
~25–80× |
| Scalar reduction over a 1-D array | mean / var / cor |
~8–40× |
| Already-vectorised gather | lookup (np.take) |
~1.1× |
The big takeaway: the Python ↔ Julia gap is operation-shaped, not language-shaped. For "already-vectorised" operations, Python is already at Julia's speed because both end up calling the same BLAS/LAPACK or hand-tuned C routine. For tight non-vectorisable recurrences, Python needs Cython to close the gap, and once it does, the residual gap is within the microbenchmark noise floor on Julia itself.
The introspection hooks¶
Every Cython port ships an <op>_is_cython() -> bool helper, re-exported at top
level. Public code that wants to assert "we're running the fast path" calls
those helpers. They are also the per-cell smoke-test that the wheel CI matrix
runs against each built artifact, so a .pyx that silently fails to compile in
a wheel cell is caught before publish.
The three-flavour benchmark¶
benchmarks/compare/ runs each scenario in three flavours and emits a JSON
record under benchmarks/compare/results/<UTC-timestamp>_<commit>.json. The
JSS paper draws on these records as the empirical backbone of Section 5.