Introduce unified nonzero_iter API across matrix types

Replaces nested column-major point lookups with a batched iterator that delegates to format-native traversal strategies. The implementation enforces a single pass per matrix type, using row-major iteration for sparse formats and eager collection for packed/columnar layouts while preserving original slot ordering. Memory allocation is optimized by removing `n_cols`-wide buffers in favor of per-row buffering or lazy iteration. Correctness tests verify iterator output against dense baselines across all supported layouts, and architecture documentation is updated to reflect the new format-agnostic query pattern.
This commit is contained in:
Eric Coissac
2026-08-20 14:07:09 +02:00
parent a4eb20e67e
commit 82ddeaddcd
8 changed files with 312 additions and 57 deletions
+51
View File
@@ -882,3 +882,54 @@ free to drift apart, and they did. If `fill_sub_matrix` itself is
rewritten as "drain `nonzero_iter`, scatter into `out[][]`", there is only
one traversal per format left to get right — the bug class doesn't just
get fixed once, it stops being possible to reintroduce.
## Implemented (2026-08-20)
Built as designed above, with one deviation from the original sketch:
`nonzero_iter` ended up `Box<dyn Iterator<...>>`, not a bare `impl
Iterator`, because `Columnar`/`Packed`/`Sparse`/`Implicit` are genuinely
different concrete types and this method isn't on a trait (kept off
`BinaryMatrix` deliberately — that trait is used as `dyn BinaryMatrix` in
`tests/sparse.rs`, and RPITIT methods aren't dyn-compatible). One `Box`
per `nonzero_iter` call, not per cell — negligible next to what it
replaces.
- `BitSliceView::nonzero_among_sorted` / `IntSliceView::nonzero_among_sorted`
(`obicompactvec/src/views.rs`): the vector-level `filter`/`filter_map`
primitive, exactly as sketched — no new state machine, `std`'s own.
- `PersistentSparseBitMatrix::nonzero_iter` (`bitmatrix/sparse.rs`): native,
`std::iter::from_fn` over one buffered row at a time via the existing
`for_each_genome_in_row` — no `n_cols`-wide allocation, ever.
- `PersistentBitMatrix::nonzero_iter` (`bitmatrix/persistent.rs`): dispatches
to the above for `Sparse`; for `Columnar`/`Packed`, loops columns,
collects each column's `nonzero_among_sorted` hits via `.extend()` (not
`flat_map` — a `flat_map` closure can't lazily return something
borrowing its own captured sort permutation across separate calls
without either boxing per-column or fighting the borrow checker; eager
collection into one `Vec` sidesteps it, at zero cost since
`fill_sub_matrix` already fully materialized anyway). `Implicit` trivial.
- `PersistentBitMatrix::fill_sub_matrix` and `sub_matrix` rewritten to
drain `nonzero_iter` — the dispatch bug is gone because there is now
only one traversal per format, not because the old one was patched.
`PersistentCompactIntMatrix::nonzero_iter` added the same way (counts
not excluded, per the earlier ask) — no native low-effort case, since no
sparse count format exists, but on the same primitive, ready for one.
- `KmerPartition::query_partition_with` (`obikpartitionner/src/query_layer.rs`):
stage 2's column-major `for g { for slot { col_value } }` replaced by one
`layer.nonzero_iter(&slot_list)` call per layer, format-agnostic.
- Tests: `nonzero_iter_matches_dense`, `nonzero_iter_matches_row`, and —
the one that actually targets the dispatch bug rather than each type's
own correctness — `enum_wrapper_dispatches_to_native_sparse` (builds
`PersistentBitMatrix::Sparse(...)` directly, not through `open`, since
`open` only auto-detects `Sparse` from a `presence/` dir layout).
`cargo test --workspace`: green, no regressions.
**Measured**: re-ran the `benchmark/` query branch (100k reads × 2
specimens, same setup as the original finding). Correctness still 0
mismatches. The dense/sparse performance gap is gone — previously sparse
~30-50% slower than dense, reproducibly; now within ~1-3% either way
(7.42s dense vs 7.60s sparse for `Escherichia_coli--K-12_MG1655`; 5.25s vs
5.30s for `Saccharolobus_islandicus--M.16.4`) — noise-level, not a
systematic gap. `pack --sparse`'s claimed query win isn't confirmed
outright by this (sparse should arguably now *beat* dense on truly sparse
real data, not just tie), but the pathological regression is fixed.