Skip to content

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:

  • encodegeo2mort(lats, lons, order): lat/lon -> uint64 morton indices.
  • decodemort2geo(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/s is 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 ms is 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). Flat morton_coverage is 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.