Workspace perf audit — zero-copy deviants & speed blunders (2026-06-11)
Six parallel source audits across the decompressors (linflate, lgz, lbzip2, ljar, lzip-parallel) and the core (de)compress pipeline (znippy-compress, znippy-common, gatling). Triggered after the stree fix; each agent specifically hunted the stree antipattern (per-call is_x86_feature_detected! + non-inlinable #[target_feature]). Findings are ranked; every item has file:line in the agent transcripts.
P0 — biggest levers (cross-cutting)
-
The whole build runs at SSE2 — AVX2 code paths are dead.
.cargo/config.tomlsets onlytarget-dir; notarget-cpu/target-feature.linflate/src/copy.rs:117copy_chunks::<32>is a plain#[inline(always)]generic that inlines into the non-featureinflate_fast, so LLVM lowers the 32-byte match copy to 2×movdqu(SSE2), notvmovdqu ymm— the module doc's "N=32 → ymm" claim is false for the shipped build. This is the stree bug's whole class, workspace-wide. Fix: build local binaries withtarget-cpu=x86-64-v3(free ~1.5–2× as proven for stree), or add AVX2#[target_feature]variants for the hot copy (mirroring the existingcopy_chunks_avx512). Same reproducibility caveat as stree — don't commit a global flag; set it for the local planet/release build. -
stree antipattern replicated in the flush scanners.
lgz/src/deflate_scan.rs:30-46(find_next_flush) —is_x86_feature_detected!runs per call, andfind_next_valid_flush(:165) calls it in awhile letskip loop once per false-positive candidate (~thousands per 200 MB chunk). Genuine hot loop. Fix: fold the candidate-skip loop inside one#[target_feature]AVX2 context (resolve detection once), exactly the stree fix.ljar/src/deflate_scan.rs:37-48— per-call infind_all_flushes(per segment, benchmarked ininflate_bench.rs). Same fix.lbzip2,lzip-parallel, and the compress pipeline are clean — their SIMD detection is already hoisted (or compile-time_mm_prefetch).
P1 — Law-1 violations on common paths
- STORE entries do a full
.to_vec()—ljar/src/entry.rs:73andlzip-parallel/src/entry.rs:73. Already-compressed entries (.png, nested.jar,.so) are memcpy'd from a buffer that outlives the call. Should returnCow::Borrowed. This is GB/s of needless copy on store-heavy archives. - slot_packer skip-path memcpy —
znippy-compress/src/slot_packer.rs:557-590. On theskip/incompressible branch (the full-I/O-speed common case for.jar/.gz/.parquet) the worker copies the whole slot into an ownedVecjust to release the slot early.stream_packer.rs::Payload::Skipalready shows the zero-copy fix (carry slot id,pwritefrom source, release after). Biggest win in the compress path. - Streaming readers double-clone names + per-entry
vec![0u8; n]—ljar/src/reader.rs:96-126,lzip-parallel/src/reader.rs:153-164. Name cloned twice (std::mem::takeinstead); read buffer zero-filled then overwritten (uninitset_lenlikechunk.rsalready does); not reused across batches.
P2 — allocation churn / double copies
- lgz
decode_chunkdouble-copy —lgz/src/chunk.rs:159-167: segments intoVec<Vec<u8>>, then every byte memcpy'd again into an un-presizedresult. - lbzip2
decompress_parallelper-blockVec+ ~1 GB final concat —lbzip2/src/parallel.rs:67-70; route through the segment path (chunk.rsalready decodes into a pre-sized output). NB the bench measures this slower path, possibly not the productionchunkpath. - lbzip2
par_mapper-slotMutex—lbzip2/src/par.rs:42:nMutex allocs +nlocks for a disjoint-index scatter; useUnsafeCell/raw-ptr scatter (zero sync). - stream_packer unbounded channel —
znippy-compress/src/stream_packer.rs:90:unbounded()ofArchiveEntry{data: Vec<u8>}→ no backpressure, unbounded in-flight payloads. Make itbounded(workers·k). - io_uring per-batch allocations —
slot_packer.rsrun_small_pass:cstrings/fds/offsets/read_resultsreallocated per 128-file batch, plus(0..n).collect::<Vec<_>>().chunks(256)(lines 411/447) allocates a throwaway index Vec just to chunk a range. Hoist + reuse; use.step_by(256). - Big-file metadata re-reads whole file —
slot_packer.rs:289std::fs::read(path)per plugin-matched big file, after it already streamed through the slot pool. Extract from header bytes in-slot, or reuse one buffer. - Per-segment
vec![0u8; estimate]zero-fill +.to_vec()result —lgz/src/speculative.rs:199-214,lzip-parallel/src/chunk.rsdecode_segment. Reuse the uninitwith_capacity+set_lenpattern already inlgz/chunk.rs.
P3 — algorithmic / micro
- lbzip2 MTF byte-at-a-time —
lbzip2/src/mtf.rs:39copy_within(..idx,1)shifts up to ~255 bytes per symbol (~900K symbols/block): the dominant bzip2 decode cost. Blocked/SIMD MTF (libbzip2 MTFA/MTFV, or register-shift foridx<16) is the biggest single decode win. - lbzip2
tt.push/resizeper symbol —block.rs:204/219: raw writes into the pre-sized buffer + one hoisted bound check (RLE2 stage already does this). - linflate micro: 14 KB fixed-table memcpy per fixed block (
fixed.rs:55); dist 8..31 scalar (could AVX2-tile,copy.rs:55); dead codeextract_var(BMI2, unused) +compute_code_for_sym. - ljar central-dir
Stringper entry even when filtered out —central_dir.rs:140: allocate-then-filter; borrow&strfromcd_data.
Verified CLEAN (don't touch)
- Core zero-copy is sound: the coalescing
ChunkRevolver(znippy-zoomies) sends slot ids/ptrs not payloads; Gatling typed mode;decompress.rs(reused buffers, positioned I/O, lock-free cursor). Laws 1/3/4 hold there. - blake3 one-shot over a contiguous slice (SIMD dispatched once) — incremental
.update()would be no faster. - lbzip2 BWT inverse is O(n) counting sort (not O(n log n)); RLE2 raw-ptr + prefetch; bitreaders are 64-bit branchless refill,
#[inline(always)]. - No stree antipattern in lbzip2 / lzip-parallel / compress / codec.
Benchmark coverage
| crate | nornir release gate | per-crate criterion + history |
| linflate | ✓ guava + scala (InflateCmp) | ✗ none |
| ljar | ✓ guava + scala | ✓ inflate/jar_throughput/chunk_into |
| lzip-parallel | ✓ guava + scala | ✓ zip_throughput (A/B vs zip) |
| lbzip2 | guava only | ✓ decompress_bench (but tests the slower parallel path) |
| lgz | guava only | ✗ none |
Gaps to close:
- nornir: add
lbzip2_scala,lgz_scala(done in this commit). - per-crate:
lgzandlinflatehave no local bench/history — the two crates with the worst SIMD findings (P0.1, P0.2) have nothing to A/B against. Per the project's benchmark-first law, add these before the P0/P1 fixes. - No bench exercises STORE-heavy archives (P1.3) or the streaming readers (P1.5).
Results (2026-06-11) — measured, not assumed
Benchmark-first paid off and overturned the audit's top theory.
- P0.1 (linflate AVX2) — REJECTED by benchmark. Added a criterion bench (
benches/inflate.rs: text/binary/rle corpora) and tried the obvious fix — multiversioninflate_fastso the whole decode loop runs in a#[target_feature(enable="avx2")]context (making the inlinedcopy_chunks::<32>emitymm). Controlled quiet A/B: it regressed every corpus (reverting it was +5.3% text, +4.0% rle, +1.1% binary). Reason: real DEFLATE decode is Huffman-bit-decode-bound, not match-copy-bound — forcing the scalar bit-decode into an AVX2 codegen context costs more (register pressure / instruction selection) than the wider copy saves. The audit's "biggest lever" was theoretical; the workload disagrees. Reverted. linflate keeps the new bench + dead-code cleanup; its real win is atarget-cpu=nativebuild (general codegen), documented, not forced. Lesson: SIMD-everything ≠ faster; measure. - P0.2 (lgz/ljar scanner dispatch) — different shape (removing per-call overhead in a hot loop, like the stree fix, not wrapping a loop in AVX2) — implemented, benched separately.
- lbzip2 (P3.13/14, P2.7/8): MTF small-index fast path (u128 shift, with an exhaustive equivalence test),
decompress_parallelrouted through the segment path (no per-blockVec, concat N→~128), lock-free scatter (no per-item Mutex), rawttwrites. 26 tests + byte-exact-vs-libbz2 green; no regression on the Huffman-bound test file, wins scale with block count / MTF-heavy data. - znippy-compress (P1.4, P2.9/10/11): zero-copy
Payload::Slotskip path (writer pwrites from the slot, releases after — kills the stored-raw memcpy), bounded entry channel, hoisted/reused io_uring batch buffers +step_byinstead of(0..n).collect().chunks(), reused big-file metadata buffer. 35 integration tests green; compress→decompress→verify roundtrip byte-exact. - lgz (P0.2, P2.6/12): scanner dispatch hoisted to a
OnceLockfn-pointer with the candidate-skip loop folded into thetarget_featurevariants (no per-call detection); presizeddecode_chunk; uninit speculative buffers. 22 tests green, decoded output byte-identical. - ljar (P0.2, P1.3/5): scanner dispatch hoisted; STORE entries borrow via
Cowon the batch-write path; reader name moved once (into_par_iter) + uninit read buffer. 57 tests green. The scanner change A/B'd neutral (±0.5%, within noise — the bench corpora aren't flush-dense) — kept because it removes the antipattern with no regression and helps flush-dense streams. - lzip-parallel (P1.3/5): STORE
Cowborrow + directwrite_allon the batch path; uninit read buffer + single name move. 56 tests green. - linflate micro: dead
compute_code_for_symmade#[cfg(test)](was a test-only helper triggering a dead-code warning).
Build lever (P0.1, the real linflate/SIMD win): since in-code AVX2 dispatch regressed, the win is a native-ISA build — proven ~1.5–2× for stree, and it lets LLVM widen copy_chunks etc. for free. Now committed in .cargo/config.toml as rustflags = ["-C","target-cpu=native"]. Safe because a from-source workspace build runs on the machine it was built on (build==run), and .cargo/config.toml is not part of any published crate package — crates.io consumers build with their own flags, unaffected. Override per-invocation with RUSTFLAGS= for a portable binary.
All six crates + the znippy-tests integration suite + znippy-cli build green. No version bumps yet (publishing the changed decompressor crates is a separate gated step — the nornir release bench can't run while the sibling tree is broken).