Benchmarks
Absolute throughput for the core morton operations across representative HEALPix
orders. mortie's morton core is Rust-only — there is no Python fallback to
compare against (the Rust extension is the sole runtime path), so these are raw
numbers, not a speedup ratio.
The two headline operations are the coordinate <-> morton conversions:
- encode —
geo2mort(lats, lons, order): lat/lon ->uint64morton indices. - decode —
mort2geo(morton): morton indices -> lat/lon of the cell centre.
Both are reported as millions of morton indices per second (M idx/s),
measured over a fixed-seed array of one million coordinates spanning the sphere.
Higher is better; the order-encoded morton means throughput is nearly flat
across orders (the work is the same per element).
The coverage column times morton_coverage_moc — the compact mixed-order
polygon cover — on a small fixed box. It scales with the polygon's boundary
length in cells rather than element count, so unlike encode/decode it grows
steeply with order; see coverage_methods.md for the
flat-vs-MOC / precision / budget trade-offs and a per-method matrix.
Cross-order throughput
The table below is regenerated by bench_cross_order.py (run from the repo
root) — it writes itself in place between the markers:
| order | encode (M idx/s) | decode (M idx/s) | coverage (cells / ms) |
|---|---|---|---|
| 4 | 47.9 | 13.1 | 1c / 0ms |
| 12 | 38.9 | 10.5 | 4c / 0ms |
| 18 | 29.9 | 9.3 | 186c / 1ms |
| 29 | 26.9 | 7.6 | 419,292c / 4306ms |
Encode/decode throughput measured over 1,000,000 fixed-seed coordinates; coverage over a fixed ~0.01 degree box. M idx/s = millions of morton indices per second, c = cell count, ms = milliseconds. Throughput and timings are machine/run dependent; cell counts are deterministic.
Notes on the numbers
- Cell/coverage counts are deterministic (a fixed seed and a fixed polygon); throughput and timings are machine/run dependent — treat them as order-of-magnitude, not reproducible constants.
- encode/decode are element-wise, so
M idx/sis roughly order-independent; the coordinate array is one million points and each order re-encodes the same points. Each throughput figure is the warm median of five timed runs. - The coverage
msis a warm median (five reps at cheap orders; a single timed call at order 29, which is seconds-scale) taken after a throwaway warm-up call — see First-call warm-up below. The cell count beside it is exact and reproducible. - Coverage is deliberately scoped small. The coverage input is a fixed
~0.01 degree (~1 km) box, chosen so order 29 stays tractable. Coverage cost
grows with boundary length measured in cells (~2**order per boundary edge for
the MOC), so a large real-world polygon covered at order 29 would be far more
expensive — cover it at a working order or with a
max_cells=budget instead (see coverage_methods.md). Flatmorton_coverageis not benchmarked cross-order because it scales as ~4**order along the boundary and exhausts memory well before order 29.
First-call warm-up
The first call into the extension pays a one-time cost — the rayon
threadpool spins up and caches are cold — that later calls do not. The size of
the penalty is inversely proportional to the per-call work, so it matters most
for small, latency-sensitive calls:
| operation (measured) | cold (first call) | warm (steady state) | ratio |
|---|---|---|---|
morton_coverage_moc, ~1 km box, order 11 |
~1.1 ms | ~0.2 ms | ~5x |
mort2geo, 1M indices, order 12 |
~196 ms | ~132 ms | ~1.5x |
geo2mort, 1M indices, order 12 |
~47 ms | ~44 ms | ~1.05x |
So a big batch encode/decode barely notices it, but a small cover (or a single point, or a tight per-call loop) can run several times slower on its first invocation. Two practical consequences:
- When benchmarking, make one throwaway call before timing (this page's generator warms the extension once up front, then reports the steady state).
- In production, if first-call latency matters (a request path, an interactive tool), warm the functions you use once at startup with a small throwaway call; steady-state throughput is what the table above reports.
Regenerating benchmarks
Both benchmark tables regenerate themselves in place, between HTML-comment
markers, on the current build. Rebuild the Rust extension first
(maturin develop --release, see
BUILDING.md), then run
from the repo root:
python bench_cross_order.py # this page: cross-order encode/decode/coverage
python bench_matrix.py # docs/coverage_methods.md: coverage-method matrix
Each script prints its table and rewrites its target doc between the markers, so committing the doc after a run keeps the published numbers current. Cell counts are deterministic; timings are machine dependent — regenerate on the machine you want to quote.