Store minorant flag in family mask to avoid costly k-mer reconstruction
Transition the minorant flag from a derived value to a stored field within the family mask, resolving a performance regression where on-the-fly reconstruction consumed significant query time. This change introduces O(1) k-mer reconstruction APIs, shifts minorant computation to the index build phase, and enables direct annex-based statistics. Supporting updates include adopting shared ownership for partition caches and refactoring batch processing pipelines.
This commit is contained in:
@@ -1624,10 +1624,33 @@ a fixed reference. Storing instead a **4-bit mask** — one bit per base
|
||||
family's fixed canonical form, i.e. the member with `A` at the centre — see
|
||||
above) is observed anywhere in the index — fixes both:
|
||||
- **Sibling count is derived, not stored**: `siblings = popcount(mask) - 1`.
|
||||
- **Minorant is derived, not stored**: regenerate the family's 4 canonical
|
||||
- ~~**Minorant is derived, not stored**: regenerate the family's 4 canonical
|
||||
forms from the slot's own k-mer (cheap, no lookup — see above), compare
|
||||
the raw encodings of whichever bits are set in the mask, take the
|
||||
smallest.
|
||||
smallest.~~ **Erratum (2026-08-14) — this was wrong, kept struck through
|
||||
rather than deleted.** "Cheap, no lookup" only accounts for the bit
|
||||
algebra (regenerate 4 forms, compare raw encodings) — true in isolation,
|
||||
but it silently assumed "the slot's own k-mer" is a free fact. It isn't:
|
||||
getting from an MPHF slot index back to the actual k-mer sequence means
|
||||
reconstructing `slot_kmer` for the whole layer — scan `unitigs.bin`,
|
||||
`mphf.find()` every k-mer to place it — an O(distinct k-mers) pass
|
||||
through the MPHF, not a per-slot O(1) lookup. That reconstruction is free
|
||||
*only* when the caller already needs k-mer identity for something else in
|
||||
the same traversal (e.g. the SNP sweep below, which needs it anyway to
|
||||
generate `central_canonical_neighbors()`). A caller that wants *only* the
|
||||
minorant flag pays the full reconstruction for nothing: measured on a real
|
||||
run, a bare family-size histogram (four buckets, otherwise near-instant)
|
||||
spent 71% of wall-clock in `MphfLayer::find`, all of it solely to answer
|
||||
"is this slot the minorant". **Current design: minorant *is* stored**
|
||||
after all — a 5th mask bit (4 presence bits + 1 minorant bit, still fits
|
||||
one byte alongside the presence mask below), written once in the
|
||||
construction pass where the k-mer is already in hand for other reasons
|
||||
(`obikindex::siblings::build_layer_sibling_annex`), read back for free by
|
||||
every later consumer (`FamilyMask::is_minorant`). This is genuinely a
|
||||
return to the superseded 3-bit design's core idea (store minorant
|
||||
alongside the count) — the "which variant" blindness that motivated
|
||||
moving away from it is fixed by keeping the full 4-bit presence mask
|
||||
too, not by dropping the stored minorant bit again.
|
||||
- **A future consumer knows exactly which variants to (re-)query** —
|
||||
`popcount(mask) - 1` lookups instead of always 3, and it knows *which*
|
||||
3 (or fewer) to issue, not just how many hits to expect.
|
||||
@@ -1644,7 +1667,11 @@ above) is observed anywhere in the index — fixes both:
|
||||
per-slot packed array (the presence mask above), one per partition — same
|
||||
on-disk shape family as `PersistentBitMatrix`'s `Packed` variant, but
|
||||
simpler (no per-genome columns, a single derived read-only value per
|
||||
slot).
|
||||
slot). As actually implemented (`obicompactvec::siblingannex`): **not**
|
||||
truly bit-packed — 1 byte/slot, 4 presence bits + the minorant bit (see
|
||||
the erratum above) in the low 5 bits, 3 unused. Deliberately simpler for
|
||||
a first implementation; packing to 5 bits/slot is a pure storage-density
|
||||
follow-up, not a behavioural change, still not done as of this note.
|
||||
<details><summary>Superseded 3-bit design (historical)</summary>
|
||||
3 bits, storing minorant status alongside sibling count directly, since
|
||||
it came for free from the same lookups (point 3 below) — 5 real states
|
||||
|
||||
Reference in New Issue
Block a user