diff --git a/src/Cargo.lock b/src/Cargo.lock index 02572b44..b90d56a3 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -300,7 +300,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af737c6c59cb018ecbe6472cbdf86d39c59d78252febfe311953a991b6e4ed85" dependencies = [ - "common_traits", + "common_traits 0.11.4", "epserde", "mem_dbg", ] @@ -475,7 +475,18 @@ checksum = "fda9ae1f26adcae83adb2e92f69cf59421f2a277a942f49f8e59f2fcbd7cf062" dependencies = [ "anyhow", "half", - "impl-tools", + "impl-tools 0.10.3", +] + +[[package]] +name = "common_traits" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e0d7d0c5977b1e87f759d406740c38227125f374f54d81bc197e877f2c5f26" +dependencies = [ + "anyhow", + "half", + "impl-tools 0.12.0", ] [[package]] @@ -783,7 +794,7 @@ checksum = "c40d342ff20a2ce62d9a85ce406e672dfa137f902ac9670034533184f1533976" dependencies = [ "anyhow", "bitflags 2.11.1", - "common_traits", + "common_traits 0.11.4", "epserde-derive", "maligned", "mem_dbg", @@ -1083,11 +1094,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae95c9095c2f1126d7db785955c73cdc5fc33e7c3fa911bd4a42931672029a7" dependencies = [ "autocfg", - "impl-tools-lib", + "impl-tools-lib 0.11.4", "proc-macro-error2", "syn 2.0.117", ] +[[package]] +name = "impl-tools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c8567c75b167cdda5ecee024da9cc9581d41bd52460e2e0e768967972aeef1" +dependencies = [ + "autocfg", + "impl-tools-lib 0.12.0", + "proc-macro-error3", + "syn 2.0.117", +] + [[package]] name = "impl-tools-lib" version = "0.11.4" @@ -1100,6 +1123,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "impl-tools-lib" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c81eb99ba7c6145aeccb8857ddc0c611cd52440091dcfddd605b6a930c9afbf4" +dependencies = [ + "proc-macro-error3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1519,7 +1554,7 @@ dependencies = [ name = "obicompactvec" version = "0.1.0" dependencies = [ - "common_traits", + "common_traits 0.13.0", "memmap2", "ndarray", "rayon", @@ -2009,6 +2044,16 @@ dependencies = [ "quote", ] +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro-error2" version = "2.0.1" @@ -2020,6 +2065,17 @@ dependencies = [ "quote", ] +[[package]] +name = "proc-macro-error3" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2093,7 +2149,7 @@ dependencies = [ "cacheline-ef", "clap", "colored", - "common_traits", + "common_traits 0.11.4", "epserde", "epserde-derive", "fastrand", diff --git a/src/obicompactvec/Cargo.toml b/src/obicompactvec/Cargo.toml index 8ccc2195..43d8e42b 100644 --- a/src/obicompactvec/Cargo.toml +++ b/src/obicompactvec/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -common_traits = "0.11" +common_traits = "0.13" memmap2 = "0.9" ndarray = "0.17" rayon = "1" diff --git a/src/obicompactvec/src/bitmatrix/persistent.rs b/src/obicompactvec/src/bitmatrix/persistent.rs index b61fcefa..08956f5d 100644 --- a/src/obicompactvec/src/bitmatrix/persistent.rs +++ b/src/obicompactvec/src/bitmatrix/persistent.rs @@ -200,7 +200,7 @@ impl PersistentBitMatrix { /// buffer, ever — the whole point, see /// `PersistentSparseBitMatrix::nonzero_iter`); `Columnar`/`Packed` /// reuse the same sorted-slot batching `fill_sub_matrix` always used - /// (`BitSliceView::nonzero_among_sorted`), just eagerly collected here + /// (`BitSliceView::enumerate_nonzero_slots`), just eagerly collected here /// rather than filled into a dense buffer — same `get` calls, same /// mmap locality, no per-format duplication of that logic elsewhere. /// `Implicit` is trivial (one column, always present). @@ -228,7 +228,7 @@ impl PersistentBitMatrix { for c in 0..n_cols { let view = self.col_view(c); hits.extend( - view.nonzero_among_sorted(&sorted_slots) + view.enumerate_nonzero_slots(&sorted_slots) .map(|pos| (perm[pos], c, 1u32)), ); } diff --git a/src/obicompactvec/src/bitvec.rs b/src/obicompactvec/src/bitvec.rs index 58297ee9..a9737811 100644 --- a/src/obicompactvec/src/bitvec.rs +++ b/src/obicompactvec/src/bitvec.rs @@ -75,36 +75,15 @@ impl PersistentBitVec { /// The slots are sorted internally before reading so that accesses to /// the underlying mmap are as sequential as possible, then the results /// are reordered to match the input order. - pub fn get_batch(&self, slots: &[usize]) -> Vec { - let mut out = vec![false; slots.len()]; - self.fill_batch(slots, &mut out); - out + #[inline] + pub fn collect_slots_values(&self, slots: &[usize]) -> Vec { + self.view().collect_slots_values(slots) } - /// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer. - pub fn fill_batch(&self, slots: &[usize], out: &mut [bool]) { - assert_eq!(slots.len(), out.len()); - let n = slots.len(); - if n == 0 { - return; - } - let mut perm: Vec = (0..n).collect(); - perm.sort_by_key(|&i| slots[i]); - let sorted: Vec = perm.iter().map(|&i| slots[i]).collect(); - let mut tmp = vec![false; n]; - self.fill_batch_sorted(&sorted, &mut tmp); - for (i, &orig_idx) in perm.iter().enumerate() { - out[orig_idx] = tmp[i]; - } - } - - /// Fill `out` assuming `sorted_slots` is already in ascending order. - /// Results are written in `sorted_slots` order (no reordering). - pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) { - assert_eq!(sorted_slots.len(), out.len()); - for (i, &slot) in sorted_slots.iter().enumerate() { - out[i] = self.get(slot); - } + /// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer. + #[inline] + pub fn fill_slots_values(&self, slots: &[usize], out: &mut [bool]) { + self.view().fill_slots_values(slots, out) } // SAFETY: mmap is page-aligned, HEADER_SIZE=16 divisible by 8 → u64-aligned. diff --git a/src/obicompactvec/src/intmatrix.rs b/src/obicompactvec/src/intmatrix.rs index 5efe5925..8d7cff3c 100644 --- a/src/obicompactvec/src/intmatrix.rs +++ b/src/obicompactvec/src/intmatrix.rs @@ -372,7 +372,7 @@ impl PersistentCompactIntMatrix { let mut out: Vec> = Vec::with_capacity(n_cols); for c in 0..n_cols { let mut col_buf = vec![0u32; slots.len()]; - self.col_view(c).fill_batch(slots, &mut col_buf); + self.col_view(c).fill_slots_values(slots, &mut col_buf); out.push(col_buf); } out @@ -397,7 +397,7 @@ impl PersistentCompactIntMatrix { for (c, col) in out.iter_mut().enumerate() { col.resize(n, 0); let mut tmp = vec![0u32; n]; - self.col_view(c).fill_batch_sorted(&sorted_slots, &mut tmp); + self.col_view(c).fill_slots_values_sorted(&sorted_slots, &mut tmp); for (i, &orig_idx) in perm.iter().enumerate() { col[orig_idx] = tmp[i]; } @@ -411,7 +411,7 @@ impl PersistentCompactIntMatrix { /// design even though no sparse count format exists yet). No native /// low-effort case here the way `PersistentSparseBitMatrix` has one — /// both variants reuse the same sorted-slot batching `fill_sub_matrix` - /// already used, via `IntSliceView::nonzero_among_sorted`. + /// already used, via `IntSliceView::enumerate_nonzero_slots`. pub fn nonzero_iter<'a>(&'a self, slots: &'a [usize]) -> impl Iterator + 'a { let n = slots.len(); let mut perm: Vec = (0..n).collect(); @@ -422,7 +422,7 @@ impl PersistentCompactIntMatrix { for c in 0..n_cols { hits.extend( self.col_view(c) - .nonzero_among_sorted(&sorted_slots) + .enumerate_nonzero_slots(&sorted_slots) .map(|(pos, v)| (perm[pos], c, v)), ); } diff --git a/src/obicompactvec/src/reader.rs b/src/obicompactvec/src/reader.rs index 0cc00c45..78f51e4a 100644 --- a/src/obicompactvec/src/reader.rs +++ b/src/obicompactvec/src/reader.rs @@ -67,34 +67,15 @@ impl PersistentCompactIntVec { /// The slots are sorted internally before reading so that accesses to /// the underlying mmap are as sequential as possible, then the results /// are reordered to match the input order. - pub fn get_batch(&self, slots: &[usize]) -> Vec { - let mut out = vec![0u32; slots.len()]; - self.fill_batch(slots, &mut out); - out + #[inline] + pub fn collect_slots_values(&self, slots: &[usize]) -> Vec { + self.view().collect_slots_values(slots) } - /// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer. - pub fn fill_batch(&self, slots: &[usize], out: &mut [u32]) { - assert_eq!(slots.len(), out.len()); - let n = slots.len(); - if n == 0 { return; } - let mut perm: Vec = (0..n).collect(); - perm.sort_by_key(|&i| slots[i]); - let sorted: Vec = perm.iter().map(|&i| slots[i]).collect(); - let mut tmp = vec![0u32; n]; - self.fill_batch_sorted(&sorted, &mut tmp); - for (i, &orig_idx) in perm.iter().enumerate() { - out[orig_idx] = tmp[i]; - } - } - - /// Fill `out` assuming `sorted_slots` is already in ascending order. - /// Results are written in `sorted_slots` order (no reordering). - pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) { - assert_eq!(sorted_slots.len(), out.len()); - for (i, &slot) in sorted_slots.iter().enumerate() { - out[i] = self.get(slot); - } + /// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer. + #[inline] + pub fn fill_slots_values(&self, slots: &[usize], out: &mut [u32]) { + self.view().fill_slots_values(slots, out) } fn overflow_get(&self, slot: usize) -> u32 { diff --git a/src/obicompactvec/src/tests/bitvec.rs b/src/obicompactvec/src/tests/bitvec.rs index 7214ae69..413c4804 100644 --- a/src/obicompactvec/src/tests/bitvec.rs +++ b/src/obicompactvec/src/tests/bitvec.rs @@ -215,65 +215,65 @@ fn hamming_dist_basic() { assert_eq!(ra.hamming_dist(&rb), 2); } -// ── get_batch tests ──────────────────────────────────────────────────────────── +// ── collect_slots_values tests ──────────────────────────────────────────────────────────── #[test] -fn bitvec_get_batch_in_order() { +fn bitvec_collect_slots_values_in_order() { let bits = vec![true, false, true, false, true]; let (_dir, r) = make_bv(&bits); - let got = r.get_batch(&[0, 1, 2, 3, 4]); + let got = r.collect_slots_values(&[0, 1, 2, 3, 4]); assert_eq!(got, bits); } #[test] -fn bitvec_get_batch_out_of_order() { +fn bitvec_collect_slots_values_out_of_order() { let bits = vec![true, false, true, false, true]; let (_dir, r) = make_bv(&bits); - let got = r.get_batch(&[3, 0, 4, 1]); + let got = r.collect_slots_values(&[3, 0, 4, 1]); assert_eq!(got, vec![false, true, true, false]); } #[test] -fn bitvec_get_batch_with_duplicates() { +fn bitvec_collect_slots_values_with_duplicates() { let bits = vec![true, false, true]; let (_dir, r) = make_bv(&bits); - let got = r.get_batch(&[0, 2, 0, 1]); + let got = r.collect_slots_values(&[0, 2, 0, 1]); assert_eq!(got, vec![true, true, true, false]); } #[test] -fn bitvec_get_batch_empty() { +fn bitvec_collect_slots_values_empty() { let (_dir, r) = make_bv(&[true, false]); - let got = r.get_batch(&[]); + let got = r.collect_slots_values(&[]); assert!(got.is_empty()); } #[test] -fn bitvec_get_batch_out_of_bounds_returns_false() { +fn bitvec_collect_slots_values_out_of_bounds_returns_false() { // PersistentBitVec::get does NOT bounds-check — out-of-range slots read // whatever bit happens to be in the mmap (zero-initialised, so false). - // This is a known gap; get_batch inherits it. + // This is a known gap; collect_slots_values inherits it. let (_dir, r) = make_bv(&[true, false]); - let got = r.get_batch(&[0, 2]); + let got = r.collect_slots_values(&[0, 2]); assert_eq!(got, vec![true, false]); } -// BitSliceView get_batch (same logic, exercised through the view) +// BitSliceView collect_slots_values (same logic, exercised through the view) #[test] -fn bitslice_view_get_batch() { +fn bitslice_view_collect_slots_values() { let bits = vec![true, false, true, false, true]; let (_dir, r) = make_bv(&bits); let view = r.view(); - assert_eq!(view.get_batch(&[0, 1, 2]), vec![true, false, true]); - assert_eq!(view.get_batch(&[4, 3]), vec![true, false]); + assert_eq!(view.collect_slots_values(&[0, 1, 2]), vec![true, false, true]); + assert_eq!(view.collect_slots_values(&[4, 3]), vec![true, false]); } #[test] -fn bitslice_view_get_batch_out_of_bounds_returns_false() { +fn bitslice_view_collect_slots_values_out_of_bounds_returns_false() { // BitSliceView::get does NOT bounds-check either. let bits = vec![true, false]; let (_dir, r) = make_bv(&bits); let view = r.view(); - let got = view.get_batch(&[0, 2]); + let got = view.collect_slots_values(&[0, 2]); assert_eq!(got, vec![true, false]); } diff --git a/src/obicompactvec/src/tests/intmatrix.rs b/src/obicompactvec/src/tests/intmatrix.rs index c9ca9fad..fb05e911 100644 --- a/src/obicompactvec/src/tests/intmatrix.rs +++ b/src/obicompactvec/src/tests/intmatrix.rs @@ -400,7 +400,7 @@ fn partial_relfreq_bray_additive_across_split() { } } -// ── get_batch tests ──────────────────────────────────────────────────────────── +// ── collect_slots_values tests ──────────────────────────────────────────────────────────── fn make_pciv(counts: &[u32]) -> (tempfile::TempDir, PersistentCompactIntVec) { let dir = tempdir().unwrap(); @@ -413,49 +413,49 @@ fn make_pciv(counts: &[u32]) -> (tempfile::TempDir, PersistentCompactIntVec) { } #[test] -fn pciv_get_batch_in_order() { +fn pciv_collect_slots_values_in_order() { let counts = vec![10u32, 255, 300, 1000]; let (_dir, v) = make_pciv(&counts); - let got = v.get_batch(&[0, 1, 2, 3]); + let got = v.collect_slots_values(&[0, 1, 2, 3]); assert_eq!(got, counts); } #[test] -fn pciv_get_batch_out_of_order() { +fn pciv_collect_slots_values_out_of_order() { let counts = vec![10u32, 255, 300, 1000]; let (_dir, v) = make_pciv(&counts); - let got = v.get_batch(&[3, 0, 2, 1]); + let got = v.collect_slots_values(&[3, 0, 2, 1]); assert_eq!(got, vec![1000, 10, 300, 255]); } #[test] -fn pciv_get_batch_with_duplicates() { +fn pciv_collect_slots_values_with_duplicates() { let counts = vec![10u32, 255, 300]; let (_dir, v) = make_pciv(&counts); - let got = v.get_batch(&[0, 2, 0, 1]); + let got = v.collect_slots_values(&[0, 2, 0, 1]); assert_eq!(got, vec![10, 300, 10, 255]); } #[test] -fn pciv_get_batch_empty() { +fn pciv_collect_slots_values_empty() { let (_dir, v) = make_pciv(&[10u32, 20]); - let got: Vec = v.get_batch(&[]); + let got: Vec = v.collect_slots_values(&[]); assert!(got.is_empty()); } #[test] -fn pciv_get_batch_out_of_bounds_panics() { +fn pciv_collect_slots_values_out_of_bounds_panics() { let (_dir, v) = make_pciv(&[10u32, 20]); - let result = std::panic::catch_unwind(|| v.get_batch(&[0, 2])); - assert!(result.is_err(), "get_batch should panic on out-of-bounds slot"); + let result = std::panic::catch_unwind(|| v.collect_slots_values(&[0, 2])); + assert!(result.is_err(), "collect_slots_values should panic on out-of-bounds slot"); } -// IntSliceView get_batch (same logic, exercised through the view) +// IntSliceView collect_slots_values (same logic, exercised through the view) #[test] -fn intslice_view_get_batch() { +fn intslice_view_collect_slots_values() { let counts = vec![10u32, 255, 300, 1000]; let (_dir, v) = make_pciv(&counts); let view = v.view(); - assert_eq!(view.get_batch(&[0, 1, 2]), vec![10, 255, 300]); - assert_eq!(view.get_batch(&[3, 1]), vec![1000, 255]); + assert_eq!(view.collect_slots_values(&[0, 1, 2]), vec![10, 255, 300]); + assert_eq!(view.collect_slots_values(&[3, 1]), vec![1000, 255]); } diff --git a/src/obicompactvec/src/views.rs b/src/obicompactvec/src/views.rs index 6af0aa58..013f74c1 100644 --- a/src/obicompactvec/src/views.rs +++ b/src/obicompactvec/src/views.rs @@ -30,13 +30,13 @@ impl<'a> BitSliceView<'a> { /// /// Slots are sorted internally before reading, then results are reordered /// to match the input order. - pub fn get_batch(&self, slots: &[usize]) -> Vec { + pub fn collect_slots_values(&self, slots: &[usize]) -> Vec { let mut out = vec![false; slots.len()]; - self.fill_batch(slots, &mut out); + self.fill_slots_values(slots, &mut out); out } - /// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer. - pub fn fill_batch(&self, slots: &[usize], out: &mut [bool]) { + /// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer. + pub fn fill_slots_values(&self, slots: &[usize], out: &mut [bool]) { assert_eq!(slots.len(), out.len()); let n = slots.len(); if n == 0 { return; } @@ -44,32 +44,43 @@ impl<'a> BitSliceView<'a> { perm.sort_by_key(|&i| slots[i]); let sorted: Vec = perm.iter().map(|&i| slots[i]).collect(); let mut tmp = vec![false; n]; - self.fill_batch_sorted(&sorted, &mut tmp); + self.fill_slots_values_sorted(&sorted, &mut tmp); for (i, &orig_idx) in perm.iter().enumerate() { out[orig_idx] = tmp[i]; } } - /// Fill `out` assuming `sorted_slots` is already in ascending order. - /// Results are written in `sorted_slots` order (no reordering). - pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) { - assert_eq!(sorted_slots.len(), out.len()); - for (i, &slot) in sorted_slots.iter().enumerate() { - out[i] = self.get(slot); - } - } - - /// Positions in `sorted_slots` (not the slots themselves) whose bit is - /// set — a `filter` over `get`, not a new traversal. `sorted_slots` - /// need not actually be sorted for correctness, but callers batching - /// several columns over the same slot set sort once and reuse it for - /// cache-friendlier `get` access (see `PersistentBitMatrix::nonzero_iter`). - pub(crate) fn nonzero_among_sorted<'b>(self, sorted_slots: &'b [usize]) -> impl Iterator + 'b + /// `(position in slots, value)` for every slot. Plain `enumerate` + + /// `get`: order never affects correctness here, only the caller's cache + /// locality (see `enumerate_nonzero_slots`), so this takes `slots` as + /// given. + #[inline] + pub fn enumerate_slots_values<'b>(self, slots: &'b [usize]) -> impl Iterator + 'b where Self: 'b, { - sorted_slots.iter().enumerate() - .filter_map(move |(pos, &slot)| self.get(slot).then_some(pos)) + slots.iter().enumerate().map(move |(pos, &slot)| (pos, self.get(slot))) + } + + /// Fill `out` assuming `sorted_slots` is already in ascending order. + /// Results are written in `sorted_slots` order (no reordering). + pub(crate) fn fill_slots_values_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) { + assert_eq!(sorted_slots.len(), out.len()); + for (pos, v) in self.enumerate_slots_values(sorted_slots) { + out[pos] = v; + } + } + + /// Positions in `slots` (not the slots themselves) whose bit is set. + /// Order of `slots` never matters for correctness; callers batching + /// several columns over the same slot set sort it once beforehand + /// purely for cache-friendlier `get` access (see + /// `PersistentBitMatrix::nonzero_iter`). + pub fn enumerate_nonzero_slots<'b>(self, slots: &'b [usize]) -> impl Iterator + 'b + where + Self: 'b, + { + self.enumerate_slots_values(slots).filter_map(|(pos, v)| v.then_some(pos)) } pub fn count_ones(&self) -> u64 { @@ -184,14 +195,14 @@ impl<'a> IntSliceView<'a> { /// /// Slots are sorted internally before reading, then results are reordered /// to match the input order. - pub fn get_batch(&self, slots: &[usize]) -> Vec { + pub fn collect_slots_values(&self, slots: &[usize]) -> Vec { let mut out = vec![0u32; slots.len()]; - self.fill_batch(slots, &mut out); + self.fill_slots_values(slots, &mut out); out } - /// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer. - pub fn fill_batch(&self, slots: &[usize], out: &mut [u32]) { + /// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer. + pub fn fill_slots_values(&self, slots: &[usize], out: &mut [u32]) { assert_eq!(slots.len(), out.len()); let n = slots.len(); if n == 0 { return; } @@ -199,32 +210,40 @@ impl<'a> IntSliceView<'a> { perm.sort_by_key(|&i| slots[i]); let sorted: Vec = perm.iter().map(|&i| slots[i]).collect(); let mut tmp = vec![0u32; n]; - self.fill_batch_sorted(&sorted, &mut tmp); + self.fill_slots_values_sorted(&sorted, &mut tmp); for (i, &orig_idx) in perm.iter().enumerate() { out[orig_idx] = tmp[i]; } } - /// Fill `out` assuming `sorted_slots` is already in ascending order. - /// Results are written in `sorted_slots` order (no reordering). - pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) { - assert_eq!(sorted_slots.len(), out.len()); - for (i, &slot) in sorted_slots.iter().enumerate() { - out[i] = self.get(slot); - } - } - - /// Like [`BitSliceView::nonzero_among_sorted`]: `(position in - /// sorted_slots, value)` for every nonzero value — a `filter_map` over - /// `get`, not a new traversal. - pub(crate) fn nonzero_among_sorted<'b>(self, sorted_slots: &'b [usize]) -> impl Iterator + 'b + /// `(position in slots, value)` for every slot. See + /// [`BitSliceView::enumerate_slots_values`]: order never affects + /// correctness here. + #[inline] + pub fn enumerate_slots_values<'b>(self, slots: &'b [usize]) -> impl Iterator + 'b where Self: 'b, { - sorted_slots.iter().enumerate().filter_map(move |(pos, &slot)| { - let v = self.get(slot); - (v != 0).then_some((pos, v)) - }) + slots.iter().enumerate().map(move |(pos, &slot)| (pos, self.get(slot))) + } + + /// Fill `out` assuming `sorted_slots` is already in ascending order. + /// Results are written in `sorted_slots` order (no reordering). + pub(crate) fn fill_slots_values_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) { + assert_eq!(sorted_slots.len(), out.len()); + for (pos, v) in self.enumerate_slots_values(sorted_slots) { + out[pos] = v; + } + } + + /// Like [`BitSliceView::enumerate_nonzero_slots`]: `(position in slots, + /// value)` for every nonzero value — a `filter_map` over `get`, not a + /// new traversal. + pub fn enumerate_nonzero_slots<'b>(self, slots: &'b [usize]) -> impl Iterator + 'b + where + Self: 'b, + { + self.enumerate_slots_values(slots).filter_map(|(pos, v)| (v != 0).then_some((pos, v))) } /// Sequential merge scan: yields all n values in slot order.