Gunnar

nordisk/znippy

.nornir/benchmarks.md

# znippy-zoomies — benchmarks

> **Class:** `benchmarks` (class scorecard) · the **measured** numbers only.
> Design in [`zoomies-design.md`](zoomies-design.md); the *why* in [`zoomies-reasoning.md`](zoomies-reasoning.md); how to run in [`zoomies-guide.md`](zoomies-guide.md).

![zoomies multicore](assets/zoomies_multicore.svg)

## f0 — what the scorecard measures (and on what)

One suite, run through nornir's `Bencher` trait in `xtask/examples/nornir-bench.rs` — **not**
`cargo bench` / criterion. `cargo run -p xtask --release --example nornir-bench` prints a single
`BenchRun` JSON line; results are historized in the Iceberg warehouse by git SHA, **gated at >10%
regression**, and rendered into the README. Heavy scales gate behind `NORNIR_BENCH_SKIP_HEAVY`, the
planet scale behind `NORNIR_BENCH_SKIP_PLANET`.

**Machine for every number below: Threadripper PRO 3975WX · 32 cores · 128 MB L3.**

The headline is decompression: zoomies is **the only decompressor that is multi-core in every row**
of the capability matrix, and its serial inflate core still beats the reference. Those two tables come
first; the parallel-layout, lookup, and write-tail numbers follow.

## Decompressor capability matrix — the headline

The column that matters: **znippy-zoomies is multi-core in every row.** No other tool is.

| decode situation | **znippy-zoomies** | pigz | igzip / libdeflate / zlib | unzip | lbzip2 (C) / pbzip2 |
|---|---|---|---|---|---|
| zip / jar entries | ✅ multi-core (`ljar`, `lzip-parallel`) | — | — | ✗ single-thread | — |
| gzip, flush-marked (pigz `-i`, BGZF) | ✅ `lgz` | ✅ | ✗ single-stream | — | — |
| gzip, **arbitrary markerless** | ✅ `lgz` — **UNIQUE (speculative resync)** | ✗ | ✗ | — | — |
| concatenated gzip members | ✅ `lgz` | ◐ serial | ✗ serial | — | — |
| bzip2, any stream | ✅ `lbzip2` | — | — | — | ✅ `lbzip2(C)` peer · ◐ pbzip2 (own-output only) |

Only zoomies has a ✅ in every applicable row. The unique cell is **arbitrary markerless gzip**:
`lgz` speculatively resyncs the DEFLATE bitstream and decodes such a file in parallel — every other
tool falls back to a single stream.

## linflate vs miniz_oxide — serial inflate core

Single stream, single core, on **real class-file data** (nornir scorecard `linflate_guava` /
`linflate_scala`, each reporting paired `*_mbs` throughput and `speedup_x`):

| corpus | result |
|---|---|
| `linflate_guava` | **1.33–1.39× faster** than `miniz_oxide` |
| `linflate_scala` | (same band) |

So even before any parallelism, the shared inflate core is **1.33–1.39×** the reference decoder on
the same bytes. Parallelism (the matrix above) multiplies on top of this.

## Candidate-offset output model (B6) — A/B, ljar

163 840-entry / 10 GB corpus, **best-of-5 wall**. A = per-segment allocation (old); B = candidate-offset,
one buffer, in-place decode + forward-seek compaction. **Output byte-identical.**

| metric | A (per-segment alloc) | B (candidate-offset) |
|---|---|---|
| best-of-5 wall | 7.12 s | **6.70 s** |
| run-to-run variance | 7.1 – 11.3 s | **6.7 – 7.4 s** |

B removes the allocator churn: not only faster at the best case, but the variance collapses — the
allocator is no longer in the hot path.

## stree — id → position lookup

Sparse sorted i64 keys (OSM-shaped), **1 M random present queries**, ns/query best-of-N,
built with `RUSTFLAGS="-C target-cpu=native"`:

| dataset | binsearch | serial | best batch | speedup |
|---|---|---|---|---|
| 8 MB (in-cache) | 111 | 56 | **19** | 5.8× |
| 128 MB | 646 | 181 | **44** | 14.8× |
| 1 GB (RAM) | 1057 | 315 | **66** | 16.0× |

Harness soundness: our binary-search baseline (~1050 ns at 1 GB) matches Ragnar Groot Koerkamp's
published reference (~1150 ns std binary search → ~27 ns S-tree+batch+prefetch single-thread = his
40×; ~7 ns on 6 threads). A **default build without `target-cpu=native` is ~2× slower** — e.g. 1 GB
best batch **96 ns, 11.2×** instead of 66 ns / 16.0×.

## arrow-ipc metadata seal — the write-tail cost

Not a decompression number: the **single-threaded seal tail** that runs *after* the gatling blob
pipeline drains (baseline 2026-06-18, znippy 0.9.0). This is znippy's serial write ceiling, not a
zoomies decode path — listed so the whole scorecard is honest.

| bench | S = 10k | M = 100k | L = 1M |
|---|---|---|---|
| `footer_seal` seal_ms | 11.02 | 114.06 | 1545.15 |
| `footer_seal` Mrows/s | 0.91 | 0.88 | 0.65 |
| `subindex_write` Mrows/s | 9.29 | 10.15 | 5.11 |
| `random_lookup_cold` p50 | 0.84 ms | 19.2 ms | 281 ms |

Notes: `random_lookup` grows **~linearly with N** — a cold call re-decodes the whole lookup, with
`manifest_reads_per_lookup = 3`. `prefix_meta` is ~1% ≈ 100% at every scale. The append-capable clone
`ArrowIpcSinkAppend` is **byte-identical** to the original, worst throughput delta **+2.0%** — inside
the ≤10% gate.