Performance¶
fitscube-rs is a drop-in replacement for the original Python
fitscube that produces
bit-for-bit identical cubes (the parity test suite asserts this) while
running faster across the board.
Benchmark¶
The benchmark generates nchan single-channel float32 FITS images, then
times combine_fits for both implementations on the same inputs. The output
cubes are compared and a speedup is only reported when they agree.
Reproduce it with:
python scripts/benchmark.py --nchan 100 --size 256 --repeat 3
Results¶
Best of 3 runs, warm page cache. Times measure combine_fits only (input
generation excluded).
Hardware: Apple M4 Pro (12 cores). fitscube 2.3.1, astropy 8.0.0,
numpy 2.4.6.
Channels × image |
Cube size |
|
|
Speedup |
|---|---|---|---|---|
50 × 128² |
3 MB |
0.116 s |
0.052 s |
2.2× |
100 × 256² |
26 MB |
0.187 s |
0.113 s |
1.6× |
100 × 512² |
105 MB |
0.199 s |
0.131 s |
1.5× |
200 × 256² |
52 MB |
0.328 s |
0.223 s |
1.5× |
500 × 256² |
131 MB |
0.751 s |
0.555 s |
1.4× |
1000 × 128² |
66 MB |
1.375 s |
1.060 s |
1.3× |
100 × 1024² |
419 MB |
0.295 s |
0.243 s |
1.2× |
Why it’s faster¶
No per-image Python/
astropyoverhead. Header parsing, WCS handling, and frequency-axis construction run in compiled Rust. The win is largest for workloads with many small images, where this fixed per-image cost dominates — hence 2.2× at 50 × 128² shrinking toward parity as raw pixel I/O takes over.Streamed, single-pass writes. The output cube’s header is built in memory and written once; the data unit is laid down plane-by-plane with raw I/O over a sparse file, avoiding cfitsio’s redundant zero-fill pass.
Parallelism. Per-channel work (header reads, plane copies, beam-table assembly) is fanned out across cores with
rayon.
Memory¶
fitscube-rs streams: only a small number of channel planes are resident at
once, never the whole cube. Peak memory is roughly one plane × in-flight count
plus the header, independent of channel count.
Peak resident memory (max RSS), combine-only, inputs generated in a separate process so only the combine is measured:
Channels × image |
Cube size |
fitscube (py) |
fitscube-rs (CLI) |
fitscube-rs (Python) |
|---|---|---|---|---|
200 × 512² |
210 MB |
~155 MB |
58 MB |
82 MB |
100 × 1024² |
419 MB |
~300 MB |
159 MB |
275 MB |
The native CLI uses the least memory and is stable run-to-run. The
fitscube-rs Python binding pays the interpreter’s baseline on top of the
streaming core, so it wins at small/medium cubes and roughly ties fitscube at
large ones — use the CLI when footprint matters.
(An earlier fitscube-rs build allocated a cube-sized buffer in RAM while
assembling the header — peaking at the full cube size — which has since been
fixed; the numbers above are post-fix.)
Larger than memory¶
Both tools stream, so neither needs to hold the whole cube — but how they
write the data unit differs, and it matters once the cube exceeds RAM.
fitscube memmaps the output and writes through the mapping, which dirties the
entire output in memory; the peak footprint (dirty + anonymous pages) grows to
the full cube size. fitscube-rs writes each plane with positional
write_all_at, so written pages are flushed and reclaimed rather than charged
to the process — its footprint stays bounded by the in-flight planes regardless
of cube size.
Combine of a 26 GB cube on a 24 GB-RAM machine (420 × 4096², float32), combine-only:
Implementation |
Time |
Peak footprint |
Max RSS |
|---|---|---|---|
|
78 s |
23.5 GB |
1.6 GB |
|
19 s |
2.6 GB |
2.6 GB |
|
16 s |
1.2 GB |
— |
Once the cube exceeds RAM, fitscube’s footprint hits the memory ceiling and it
thrashes — ~4–5× slower here, and it would OOM outright on a cube comfortably
larger than RAM. fitscube-rs is unaffected: its footprint is set by plane
size × in-flight count, tunable with --max-workers (the default over-buffers
for very large planes; --max-workers 4 was both lighter and faster above).
What it does not claim¶
Huge single-plane cubes (time). Once a job is bound by raw pixel write throughput (large image dimensions, few channels), both implementations converge on
cfitsio/numpyI/O and the speedup narrows to ~1.5×.
Caveats¶
Times are best-of-3 with a warm page cache; absolute numbers vary with hardware, disk, and thread count. The relative ordering and the many-images-favor-Rust trend are stable.
The benchmark uses synthetic Gaussian-noise images with minimal headers. Real data with rich headers and per-channel beams increases the per-image parsing cost, which favors
fitscube-rsfurther.