Skip to main content

vortex_mask/intersect_by_rank/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::hint::cold_path;
5use std::sync::Arc;
6
7use vortex_buffer::BitBuffer;
8use vortex_buffer::BufferMut;
9use vortex_buffer::CpuKernel;
10use vortex_error::VortexExpect;
11
12use crate::Mask;
13use crate::MaskValues;
14use crate::MaskValuesRef;
15
16#[cfg(target_arch = "x86_64")]
17mod bmi2;
18#[cfg(target_arch = "aarch64")]
19mod sve;
20
21trait DepositBits {
22    /// Deposit the low bits of `source` into the set bits of `mask`, as PDEP does.
23    ///
24    /// `mask_count` must equal `mask.count_ones()`. The portable implementations use it to
25    /// pick a closed form, so a wrong count gives a wrong result. Bits of `source` above
26    /// `mask_count` are ignored.
27    fn deposit_bits(source: u64, mask: u64, mask_count: usize) -> u64;
28}
29
30trait SelectBit {
31    /// Position (0..63) of the `rank`-th set bit in `word`. Caller ensures
32    /// `rank < word.count_ones()`.
33    fn select_bit_position(word: u64, rank: usize) -> usize;
34}
35
36/// Portable deposit without branches on the data: closed forms for masks with at most two set
37/// or two clear bits, a byte-wise network otherwise.
38struct Portable;
39
40impl DepositBits for Portable {
41    // Inlined into the kernel loop: as a call per word it costs more than the closed forms.
42    #[allow(clippy::inline_always)]
43    #[inline(always)]
44    fn deposit_bits(source: u64, mask: u64, mask_count: usize) -> u64 {
45        debug_assert_eq!(mask_count, mask.count_ones() as usize);
46
47        match mask_count {
48            0..=2 => deposit_two_lowest(source, mask),
49            62.. => deposit_two_holes(source, mask),
50            _ => deposit_bytewise(source, mask),
51        }
52    }
53}
54
55impl SelectBit for Portable {
56    #[inline]
57    fn select_bit_position(word: u64, rank: usize) -> usize {
58        select_bit_position_portable(word, rank)
59    }
60}
61
62/// Portable deposit for a sparse `self`: a loop over the mask bits that stops once the source
63/// runs out. With a few bits per chunk it beats the fixed cost of [`Portable`].
64struct PortableLoop;
65
66impl DepositBits for PortableLoop {
67    #[inline]
68    fn deposit_bits(mut source: u64, mut mask: u64, mask_count: usize) -> u64 {
69        debug_assert_eq!(mask_count, mask.count_ones() as usize);
70
71        if mask_count <= 2 {
72            return deposit_two_lowest(source, mask);
73        }
74
75        let mut result = 0u64;
76        while mask != 0 && source != 0 {
77            let bit = mask & mask.wrapping_neg();
78            result |= bit & (source & 1).wrapping_neg();
79            source >>= 1;
80            mask &= mask - 1;
81        }
82        result
83    }
84}
85
86/// `b` in every byte of a word.
87const fn bytes(b: u8) -> u64 {
88    b as u64 * 0x0101_0101_0101_0101
89}
90
91/// Deposit into the lowest two set bits of `mask`.
92#[allow(clippy::inline_always)]
93#[inline(always)]
94fn deposit_two_lowest(source: u64, mask: u64) -> u64 {
95    let lowest = mask & mask.wrapping_neg();
96    let rest = mask ^ lowest;
97    let second = rest & rest.wrapping_neg();
98    (lowest & (source & 1).wrapping_neg()) | (second & ((source >> 1) & 1).wrapping_neg())
99}
100
101/// Deposit into a mask with at most two clear bits: the source moves up by one past each hole.
102#[allow(clippy::inline_always)]
103#[inline(always)]
104fn deposit_two_holes(mut source: u64, mask: u64) -> u64 {
105    let mut holes = !mask;
106    for _ in 0..2 {
107        let hole = holes & holes.wrapping_neg();
108        let below = hole.wrapping_sub(1);
109        source = (source & below) | ((source << 1) & !below);
110        holes ^= hole;
111    }
112    source & mask
113}
114
115/// Deposit a byte at a time.
116///
117/// Byte `j` takes its source bits from the number of mask bits in the bytes below it, which
118/// one multiply sums for all bytes. Within each byte the bits are spread by the compress network
119/// of Hacker's Delight 7-4 run backwards, as in `uN::extract_bits` of the Rust core library:
120/// three shift-and-XOR stages for 8 bits instead of six for 64.
121#[allow(clippy::inline_always)]
122#[inline(always)]
123fn deposit_bytewise(source: u64, mask: u64) -> u64 {
124    let pairs = mask - ((mask >> 1) & bytes(0x55));
125    let nibbles = (pairs & bytes(0x33)) + ((pairs >> 2) & bytes(0x33));
126    let counts = (nibbles + (nibbles >> 4)) & bytes(0x0F);
127    // Byte j holds the mask bits in bytes 0..j. The sums stay below 64, so no byte carries.
128    let below = counts.wrapping_mul(bytes(1)) << 8;
129
130    // Compress the mask within each byte. Stage i moves down by 2^i the bits whose count of mask
131    // zeros below them has bit i set; `landed` keeps where they land. Before stage i the zeros
132    // stand in aligned groups of 2^i, so the prefix XOR can start at a shift of 2^i.
133    let mut packed = mask;
134    let mut zeros = !mask;
135    let mut landed = [0u64; 3];
136    for (stage, landed) in landed.iter_mut().enumerate() {
137        let shift = 1 << stage;
138        let mut parity = zeros;
139        let mut len = shift;
140        while len < 8 {
141            parity ^= (parity << len) & bytes(0xFF << len);
142            len <<= 1;
143        }
144        let moving = packed & parity;
145        *landed = (moving >> shift) & bytes(0xFF >> shift);
146        packed ^= moving ^ *landed;
147        zeros &= !parity;
148        zeros ^= (zeros >> shift) & bytes(0xFF >> shift);
149    }
150
151    // Pack the source bits of each byte at its low end, then undo the stages.
152    let mut result = 0;
153    for byte in 0..8 {
154        let from = (below >> (8 * byte)) & 0xFF;
155        result |= ((source >> from) & 0xFF) << (8 * byte);
156    }
157    result &= packed;
158    for (stage, landed) in landed.iter().enumerate().rev() {
159        result = (result & !landed) | ((result & landed) << (1 << stage));
160    }
161    result & mask
162}
163
164#[inline]
165fn select_bit_position_portable(word: u64, mut rank: usize) -> usize {
166    debug_assert!(rank < word.count_ones() as usize);
167    let mut bit_offset = 0usize;
168    for byte in word.to_le_bytes() {
169        let count = byte.count_ones() as usize;
170        if rank < count {
171            let mut bits = byte;
172            for _ in 0..rank {
173                bits &= bits - 1;
174            }
175
176            return bit_offset + bits.trailing_zeros() as usize;
177        }
178
179        rank -= count;
180        bit_offset += 8;
181    }
182
183    debug_assert!(false, "rank out of bounds");
184    0
185}
186
187/// Rank bits of a [`BitBuffer`] read at an absolute offset.
188///
189/// The bits for a chunk of `self` start at the popcount of the chunks before it, which the
190/// kernel counts anyway, so no state carries from one read to the next. A sequential reader
191/// would refill its window behind a branch that random masks mispredict.
192struct RankBits<'a> {
193    bytes: &'a [u8],
194    start: usize,
195    /// The last 16 bytes followed by zeros. With a sparse `self` many reads start in the last
196    /// bytes, so they take a load from here rather than a copy each.
197    tail: [u8; 32],
198    tail_start: usize,
199}
200
201impl<'a> RankBits<'a> {
202    fn new(buffer: &'a BitBuffer) -> Self {
203        let bytes = buffer.inner().as_slice();
204        let tail_start = bytes.len().saturating_sub(16);
205        let mut tail = [0; 32];
206        tail[..bytes.len() - tail_start].copy_from_slice(&bytes[tail_start..]);
207        Self {
208            bytes,
209            start: buffer.offset(),
210            tail,
211            tail_start,
212        }
213    }
214
215    /// The 64 bits from `offset` on. Bits past the end of the backing bytes read as zero.
216    #[inline]
217    #[expect(clippy::cast_possible_truncation)]
218    fn read(&self, offset: usize) -> u64 {
219        let position = self.start + offset;
220        let (byte, shift) = (position / 8, position % 8);
221        // 16 bytes from `byte` hold 121 bits past `shift`, enough for any read.
222        let window = match self.bytes.get(byte..).and_then(<[u8]>::first_chunk) {
223            Some(window) => window,
224            None => {
225                // Keeps the branch: a select would put the comparisons on every load's address.
226                cold_path();
227                self.tail
228                    .get(byte - self.tail_start..)
229                    .and_then(<[u8]>::first_chunk)
230                    .unwrap_or(&[0; 16])
231            }
232        };
233        (u128::from_le_bytes(*window) >> shift) as u64
234    }
235}
236
237#[inline]
238fn mask_from_buffer(buffer: BitBuffer, true_count: usize) -> Mask {
239    let len = buffer.len();
240    if true_count == 0 {
241        return Mask::new_false(len);
242    }
243    if true_count == len {
244        return Mask::new_true(len);
245    }
246
247    Mask::Values(Arc::new(MaskValues {
248        buffer,
249        indices: Default::default(),
250        slices: Default::default(),
251        true_count,
252        density: true_count as f64 / len as f64,
253    }))
254}
255
256#[inline]
257fn push_result_chunk<D: DepositBits>(
258    result: &mut BufferMut<u64>,
259    self_chunk: u64,
260    self_count: usize,
261    rank_bits: u64,
262) {
263    let chunk = D::deposit_bits(rank_bits, self_chunk, self_count);
264    // SAFETY: callers allocate enough capacity for every output chunk.
265    unsafe { result.push_unchecked(chunk) };
266}
267
268/// Marked `#[inline(always)]` so each `#[target_feature]` wrapper in `bmi2` and `sve` gets its own
269/// fully-inlined copy compiled with that feature set, as in `vortex_buffer::bit::pack`.
270#[allow(clippy::inline_always)]
271#[inline(always)]
272fn intersect_bit_buffers<D: DepositBits>(
273    self_buffer: &BitBuffer,
274    mask_buffer: &BitBuffer,
275    true_count: usize,
276) -> Mask {
277    let len = self_buffer.len();
278    let mut result = BufferMut::with_capacity(len.div_ceil(64));
279    let rank_bits = RankBits::new(mask_buffer);
280    let self_chunks = self_buffer.chunks();
281    let mut rank_offset = 0;
282
283    for self_chunk in self_chunks.iter() {
284        let self_count = self_chunk.count_ones() as usize;
285        let source = rank_bits.read(rank_offset);
286        push_result_chunk::<D>(&mut result, self_chunk, self_count, source);
287        rank_offset += self_count;
288    }
289
290    if self_chunks.remainder_len() != 0 {
291        let self_chunk = self_chunks.remainder_bits();
292        let self_count = self_chunk.count_ones() as usize;
293        let source = rank_bits.read(rank_offset);
294        push_result_chunk::<D>(&mut result, self_chunk, self_count, source);
295    }
296
297    mask_from_buffer(
298        BitBuffer::new(result.freeze().into_byte_buffer(), len),
299        true_count,
300    )
301}
302
303#[allow(clippy::inline_always)]
304#[inline(always)]
305fn intersect_bit_buffer_by_rank_indices<D: DepositBits>(
306    self_buffer: &BitBuffer,
307    mask_indices: &[usize],
308) -> Mask {
309    let len = self_buffer.len();
310    let mut result = BufferMut::with_capacity(len.div_ceil(64));
311    let self_chunks = self_buffer.chunks();
312    let mut rank_base = 0usize;
313    let mut rank_idx = 0usize;
314
315    for self_chunk in self_chunks.iter() {
316        let self_count = self_chunk.count_ones() as usize;
317        let next_rank_base = rank_base + self_count;
318        let rank_bits = rank_bits_for_chunk(mask_indices, &mut rank_idx, rank_base, next_rank_base);
319        push_result_chunk::<D>(&mut result, self_chunk, self_count, rank_bits);
320        rank_base = next_rank_base;
321    }
322
323    if self_chunks.remainder_len() != 0 {
324        let self_chunk = self_chunks.remainder_bits();
325        let self_count = self_chunk.count_ones() as usize;
326        let next_rank_base = rank_base + self_count;
327        let rank_bits = rank_bits_for_chunk(mask_indices, &mut rank_idx, rank_base, next_rank_base);
328        push_result_chunk::<D>(&mut result, self_chunk, self_count, rank_bits);
329    }
330
331    debug_assert_eq!(rank_idx, mask_indices.len());
332
333    mask_from_buffer(
334        BitBuffer::new(result.freeze().into_byte_buffer(), len),
335        mask_indices.len(),
336    )
337}
338
339/// Walks `mask_indices` (global ranks into `self_buffer.set_bits`) and emits the corresponding
340/// positions in `self_buffer`. For each rank, advances `self_buffer`'s chunks via popcount
341/// skip-while, then locates the bit inside the current chunk with rank-select.
342///
343/// This dominates the chunk-scan paths when the mask is very sparse: cost is
344/// `O(mask.true_count() + self.len() / 64)` rather than `O(self.len() / 64)` per chunk.
345#[allow(clippy::inline_always)]
346#[inline(always)]
347fn intersect_mask_driven<S, I>(self_buffer: &BitBuffer, mask_indices: I, true_count: usize) -> Mask
348where
349    S: SelectBit,
350    I: Iterator<Item = usize>,
351{
352    let len = self_buffer.len();
353    if true_count == 0 {
354        return Mask::new_false(len);
355    }
356
357    let mut chunk_iter = self_buffer.chunks().iter_padded();
358
359    let mut current_chunk = chunk_iter.next().unwrap_or(0);
360    let mut current_count = current_chunk.count_ones() as usize;
361    let mut current_chunk_idx = 0usize;
362    let mut rank_before = 0usize;
363
364    let mut output = Vec::with_capacity(true_count);
365
366    for global_rank in mask_indices {
367        while rank_before + current_count <= global_rank {
368            rank_before += current_count;
369            current_chunk_idx += 1;
370            current_chunk = chunk_iter.next().vortex_expect("mask index out of bounds");
371            current_count = current_chunk.count_ones() as usize;
372        }
373
374        let local_rank = global_rank - rank_before;
375        let bit_pos = S::select_bit_position(current_chunk, local_rank);
376        output.push(current_chunk_idx * 64 + bit_pos);
377    }
378
379    debug_assert_eq!(output.len(), true_count);
380    Mask::from_indices(len, output)
381}
382
383#[inline]
384fn rank_bits_for_chunk(
385    mask_indices: &[usize],
386    rank_idx: &mut usize,
387    rank_base: usize,
388    next_rank_base: usize,
389) -> u64 {
390    let mut rank_bits = 0u64;
391    while let Some(&rank) = mask_indices.get(*rank_idx) {
392        if rank >= next_rank_base {
393            break;
394        }
395        rank_bits |= 1u64 << (rank - rank_base);
396        *rank_idx += 1;
397    }
398    rank_bits
399}
400
401fn intersect_by_rank_indices(len: usize, self_indices: &[usize], mask_indices: &[usize]) -> Mask {
402    Mask::from_indices(
403        len,
404        mask_indices.iter().map(|idx| {
405            // SAFETY: mask indices are ranks into self_indices, because
406            // mask.len() == self.true_count() == self_indices.len().
407            unsafe { *self_indices.get_unchecked(*idx) }
408        }),
409    )
410}
411
412/// Below this density of `self` the portable path deposits with [`PortableLoop`].
413///
414/// The crossover with [`Portable`] depends on the CPU. With random masks, it is at about 3.5 %
415/// on Neoverse V1 (Graviton3) and above 8 % on Neoverse N1 (Graviton2), where the network is
416/// slower. 1/16 is close to where the two balance. See the `random_rotating` benchmark when
417/// changing it.
418const PORTABLE_LOOP_MAX_DENSITY: f64 = 1.0 / 16.0;
419
420type IntersectBuffers = unsafe fn(&BitBuffer, &BitBuffer, usize) -> Mask;
421type IntersectRankIndices = unsafe fn(&BitBuffer, &[usize]) -> Mask;
422
423/// The bit-buffer kernel for this CPU: its deposit instruction if it has one, otherwise `P`.
424fn select_bit_buffers<P: DepositBits>() -> IntersectBuffers {
425    #[cfg(target_arch = "x86_64")]
426    {
427        if std::arch::is_x86_feature_detected!("bmi2")
428            && std::arch::is_x86_feature_detected!("popcnt")
429        {
430            return bmi2::intersect_bit_buffers_bmi2;
431        }
432    }
433    #[cfg(target_arch = "aarch64")]
434    {
435        if std::arch::is_aarch64_feature_detected!("sve2-bitperm") {
436            return sve::intersect_bit_buffers_sve2;
437        }
438    }
439    intersect_bit_buffers::<P>
440}
441
442/// The rank-indices kernel for this CPU, as in [`select_bit_buffers`].
443fn select_rank_indices<P: DepositBits>() -> IntersectRankIndices {
444    #[cfg(target_arch = "x86_64")]
445    {
446        if std::arch::is_x86_feature_detected!("bmi2")
447            && std::arch::is_x86_feature_detected!("popcnt")
448        {
449            return bmi2::intersect_bit_buffer_by_rank_indices_bmi2;
450        }
451    }
452    #[cfg(target_arch = "aarch64")]
453    {
454        if std::arch::is_aarch64_feature_detected!("sve2-bitperm") {
455            return sve::intersect_bit_buffer_by_rank_indices_sve2;
456        }
457    }
458    intersect_bit_buffer_by_rank_indices::<P>
459}
460
461#[inline]
462fn intersect_bit_buffers_dispatch(
463    self_buffer: &BitBuffer,
464    mask_buffer: &BitBuffer,
465    true_count: usize,
466    self_density: f64,
467) -> Mask {
468    static KERNEL: CpuKernel<IntersectBuffers> = CpuKernel::new(select_bit_buffers::<Portable>);
469    static SPARSE_KERNEL: CpuKernel<IntersectBuffers> =
470        CpuKernel::new(select_bit_buffers::<PortableLoop>);
471    let kernel = if self_density < PORTABLE_LOOP_MAX_DENSITY {
472        &SPARSE_KERNEL
473    } else {
474        &KERNEL
475    };
476    // SAFETY: the selector only returns kernels that are safe or whose required CPU
477    // features were probed before selection.
478    unsafe { kernel.get()(self_buffer, mask_buffer, true_count) }
479}
480
481#[inline]
482fn intersect_rank_indices_dispatch(
483    self_buffer: &BitBuffer,
484    mask_indices: &[usize],
485    self_density: f64,
486) -> Mask {
487    static KERNEL: CpuKernel<IntersectRankIndices> =
488        CpuKernel::new(select_rank_indices::<Portable>);
489    static SPARSE_KERNEL: CpuKernel<IntersectRankIndices> =
490        CpuKernel::new(select_rank_indices::<PortableLoop>);
491    let kernel = if self_density < PORTABLE_LOOP_MAX_DENSITY {
492        &SPARSE_KERNEL
493    } else {
494        &KERNEL
495    };
496    // SAFETY: the selector only returns kernels that are safe or whose required CPU
497    // features were probed before selection.
498    unsafe { kernel.get()(self_buffer, mask_indices) }
499}
500
501#[inline]
502fn intersect_mask_driven_dispatch<I>(
503    self_buffer: &BitBuffer,
504    mask_indices: I,
505    true_count: usize,
506) -> Mask
507where
508    I: Iterator<Item = usize>,
509{
510    #[cfg(target_arch = "x86_64")]
511    if std::arch::is_x86_feature_detected!("bmi2") && std::arch::is_x86_feature_detected!("popcnt")
512    {
513        // SAFETY: BMI2 and POPCNT were just detected.
514        return unsafe { bmi2::intersect_mask_driven_bmi2(self_buffer, mask_indices, true_count) };
515    }
516
517    #[cfg(target_arch = "aarch64")]
518    if std::arch::is_aarch64_feature_detected!("sve2-bitperm") {
519        // SAFETY: SVE2 BITPERM was just detected.
520        return unsafe { sve::intersect_mask_driven_sve2(self_buffer, mask_indices, true_count) };
521    }
522
523    intersect_mask_driven::<Portable, _>(self_buffer, mask_indices, true_count)
524}
525
526/// Returns whether a mask is sparse.
527///
528/// [`BitBuffer`] traversal uses `u64` words, so fewer than one selected value per word is sparse.
529fn mask_is_sparse(values: &MaskValuesRef) -> bool {
530    values.true_count().saturating_mul(64) < values.len()
531}
532
533/// Returns whether a rank mask is sparse.
534///
535/// The mask-driven path becomes worthwhile at approximately 3% mask density. Each set bit costs a
536/// select and push, but this avoids a popcount and deposit for each chunk of `self`.
537fn rank_mask_is_sparse(values: &MaskValuesRef) -> bool {
538    values.true_count().saturating_mul(32) < values.len()
539}
540
541impl Mask {
542    /// Take the intersection of the `mask` with the set of true values in `self`.
543    ///
544    /// The hot path keeps bit-buffer-backed masks as bit buffers. It scans the set bits of `self`
545    /// by rank and deposits selected rank bits into their original positions.
546    ///
547    /// # Examples
548    ///
549    /// Keep the third and fifth set values from mask `m1`:
550    /// ```
551    /// use vortex_mask::Mask;
552    ///
553    /// let m1 = Mask::from_iter([true, false, false, true, true, true, false, true]);
554    /// let m2 = Mask::from_iter([false, false, true, false, true]);
555    /// assert_eq!(
556    ///     m1.intersect_by_rank(&m2),
557    ///     Mask::from_iter([false, false, false, false, true, false, false, true])
558    /// );
559    /// ```
560    pub fn intersect_by_rank(&self, mask: &Mask) -> Mask {
561        assert_eq!(self.true_count(), mask.len());
562
563        match (self, mask) {
564            (Self::AllTrue(_), _) => mask.clone(),
565            (_, Self::AllTrue(_)) => self.clone(),
566            (Self::AllFalse(_), _) | (_, Self::AllFalse(_)) => Self::new_false(self.len()),
567            (Self::Values(self_values), Self::Values(mask_values)) => {
568                // Four dispatch cases keyed by (self density, mask density):
569                //
570                //              | mask sparse | mask dense
571                // -------------+-------------+------------
572                // self sparse  | indices     | indices
573                // self dense   | mask-driven | bit-buffer
574                if let Some(mask_indices) = mask_values.indices.get() {
575                    if let Some(self_indices) = self_values.indices.get()
576                        && mask_indices.len() < self.len().div_ceil(64)
577                    {
578                        return intersect_by_rank_indices(self.len(), self_indices, mask_indices);
579                    }
580
581                    let self_is_very_sparse = mask_is_sparse(self_values);
582                    let mask_is_very_sparse = rank_mask_is_sparse(mask_values);
583
584                    if self_is_very_sparse {
585                        return intersect_by_rank_indices(
586                            self.len(),
587                            self_values.indices(),
588                            mask_indices,
589                        );
590                    }
591
592                    if mask_is_very_sparse {
593                        return intersect_mask_driven_dispatch(
594                            self_values.bit_buffer(),
595                            mask_indices.iter().copied(),
596                            mask_values.true_count(),
597                        );
598                    }
599
600                    if mask_indices.len().saturating_mul(4) > mask.len() {
601                        return intersect_bit_buffers_dispatch(
602                            self_values.bit_buffer(),
603                            mask_values.bit_buffer(),
604                            mask_values.true_count(),
605                            self_values.density(),
606                        );
607                    }
608
609                    return intersect_rank_indices_dispatch(
610                        self_values.bit_buffer(),
611                        mask_indices,
612                        self_values.density(),
613                    );
614                }
615
616                let self_is_very_sparse = mask_is_sparse(self_values);
617                let mask_is_very_sparse = rank_mask_is_sparse(mask_values);
618
619                if self_is_very_sparse {
620                    return intersect_by_rank_indices(
621                        self.len(),
622                        self_values.indices(),
623                        mask_values.indices(),
624                    );
625                }
626
627                if mask_is_very_sparse {
628                    return intersect_mask_driven_dispatch(
629                        self_values.bit_buffer(),
630                        mask_values.bit_buffer().set_indices(),
631                        mask_values.true_count(),
632                    );
633                }
634
635                intersect_bit_buffers_dispatch(
636                    self_values.bit_buffer(),
637                    mask_values.bit_buffer(),
638                    mask_values.true_count(),
639                    self_values.density(),
640                )
641            }
642        }
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use rstest::rstest;
649    use vortex_buffer::BitBuffer;
650
651    use super::DepositBits;
652    use super::Portable;
653    use super::PortableLoop;
654    #[cfg(target_arch = "aarch64")]
655    use super::SelectBit;
656    use super::intersect_bit_buffer_by_rank_indices;
657    use super::intersect_bit_buffers;
658    #[cfg(target_arch = "aarch64")]
659    use super::sve::Sve2Bdep;
660    use crate::Mask;
661
662    #[test]
663    fn mask_bitand_all_as_bit_and() {
664        let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, true, true, true, true]));
665        let mask = Mask::from_buffer(BitBuffer::from_iter(vec![false, true, false, true, true]));
666        assert_eq!(
667            this.intersect_by_rank(&mask),
668            Mask::from_indices(5, vec![1, 3, 4])
669        );
670    }
671
672    #[test]
673    fn mask_bitand_all_true() {
674        let this = Mask::from_buffer(BitBuffer::from_iter(vec![false, false, true, true, true]));
675        let mask = Mask::from_buffer(BitBuffer::from_iter(vec![true, true, true]));
676        assert_eq!(
677            this.intersect_by_rank(&mask),
678            Mask::from_indices(5, vec![2, 3, 4])
679        );
680    }
681
682    #[test]
683    fn mask_bitand_true() {
684        let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, false, true, true]));
685        let mask = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, true]));
686        assert_eq!(
687            this.intersect_by_rank(&mask),
688            Mask::from_indices(5, vec![0, 4])
689        );
690    }
691
692    #[test]
693    fn mask_bitand_false() {
694        let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, false, true, true]));
695        let mask = Mask::from_buffer(BitBuffer::from_iter(vec![false, false, false]));
696        assert_eq!(this.intersect_by_rank(&mask), Mask::from_indices(5, vec![]));
697    }
698
699    #[test]
700    fn mask_intersect_by_rank_all_false() {
701        let this = Mask::AllFalse(10);
702        let mask = Mask::AllFalse(0);
703        assert_eq!(this.intersect_by_rank(&mask), Mask::AllFalse(10));
704    }
705
706    #[rstest]
707    #[case::all_true_with_all_true(
708        Mask::new_true(5),
709        Mask::new_true(5),
710        vec![0, 1, 2, 3, 4]
711    )]
712    #[case::all_true_with_all_false(
713        Mask::new_true(5),
714        Mask::new_false(5),
715        vec![]
716    )]
717    #[case::all_false_with_any(
718        Mask::new_false(10),
719        Mask::new_true(0),
720        vec![]
721    )]
722    #[case::indices_with_all_true(
723        Mask::from_indices(10, vec![2, 5, 7, 9]),
724        Mask::new_true(4),
725        vec![2, 5, 7, 9]
726    )]
727    #[case::indices_with_all_false(
728        Mask::from_indices(10, vec![2, 5, 7, 9]),
729        Mask::new_false(4),
730        vec![]
731    )]
732    fn test_intersect_by_rank_special_cases(
733        #[case] base_mask: Mask,
734        #[case] rank_mask: Mask,
735        #[case] expected_indices: Vec<usize>,
736    ) {
737        let result = base_mask.intersect_by_rank(&rank_mask);
738        assert_eq!(result.len(), base_mask.len());
739
740        match result.indices() {
741            crate::AllOr::All => assert_eq!(expected_indices.len(), result.len()),
742            crate::AllOr::None => assert!(expected_indices.is_empty()),
743            crate::AllOr::Some(indices) => assert_eq!(indices, &expected_indices[..]),
744        }
745    }
746
747    #[test]
748    fn test_intersect_by_rank_example() {
749        // Example from the documentation
750        let m1 = Mask::from_iter([true, false, false, true, true, true, false, true]);
751        let m2 = Mask::from_iter([false, false, true, false, true]);
752        let result = m1.intersect_by_rank(&m2);
753        let expected = Mask::from_iter([false, false, false, false, true, false, false, true]);
754        assert_eq!(result, expected);
755    }
756
757    #[test]
758    #[should_panic]
759    fn test_intersect_by_rank_wrong_length() {
760        let m1 = Mask::from_indices(10, vec![2, 5, 7]); // 3 true values
761        let m2 = Mask::new_true(5); // 5 true values - doesn't match
762        m1.intersect_by_rank(&m2);
763    }
764
765    #[rstest]
766    #[case::single_element(
767        vec![3],
768        vec![true],
769        vec![3]
770    )]
771    #[case::single_element_masked(
772        vec![3],
773        vec![false],
774        vec![]
775    )]
776    #[case::alternating(
777        vec![0, 2, 4, 6, 8],
778        vec![true, false, true, false, true],
779        vec![0, 4, 8]
780    )]
781    #[case::consecutive(
782        vec![5, 6, 7, 8, 9],
783        vec![false, true, true, true, false],
784        vec![6, 7, 8]
785    )]
786    fn test_intersect_by_rank_patterns(
787        #[case] base_indices: Vec<usize>,
788        #[case] rank_pattern: Vec<bool>,
789        #[case] expected_indices: Vec<usize>,
790    ) {
791        let base = Mask::from_indices(10, base_indices);
792        let rank = Mask::from_iter(rank_pattern);
793        let result = base.intersect_by_rank(&rank);
794        assert_eq!(result.len(), base.len());
795
796        match result.indices() {
797            crate::AllOr::Some(indices) => assert_eq!(indices, &expected_indices[..]),
798            crate::AllOr::None => assert!(expected_indices.is_empty()),
799            _ => panic!("Unexpected result"),
800        }
801    }
802
803    #[rstest]
804    // Larger sizes to push the bench-shaped buffer paths through the unit tests too.
805    #[case::dense_len_1024(1024, 31, 0.5, 0.5)]
806    // Very-sparse mask exercises the mask-driven dispatch path. Both densities live in
807    // the half-open interval where `mask_is_very_sparse` is true.
808    #[case::sparse_mask_1pct(1024, 17, 0.5, 0.01)]
809    #[case::sparse_mask_2pct(2048, 0, 0.5, 0.02)]
810    #[case::very_sparse_mask_with_offsets(513, 5, 0.5, 0.005)]
811    fn test_intersect_by_rank_density_matrix(
812        #[case] base_len: usize,
813        #[case] base_offset: usize,
814        #[case] base_density: f64,
815        #[case] rank_density: f64,
816    ) {
817        #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
818        let base_threshold = (base_density * 1024.0) as usize;
819        #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
820        let rank_threshold = (rank_density * 1024.0) as usize;
821
822        let base_source: Vec<bool> = (0..base_len + base_offset + 16)
823            .map(|i| (i * 7 + 13) % 1024 < base_threshold)
824            .collect();
825        let base_bits = base_source[base_offset..base_offset + base_len].to_vec();
826        let base = Mask::from_buffer(
827            BitBuffer::from(base_source).slice(base_offset..base_offset + base_len),
828        );
829
830        let rank_len = base.true_count();
831        let rank_bits: Vec<bool> = (0..rank_len)
832            .map(|i| (i * 11 + 7) % 1024 < rank_threshold)
833            .collect();
834        let rank_from_buffer = Mask::from_buffer(BitBuffer::from(rank_bits.clone()));
835        let rank_indices_vec = rank_bits
836            .iter()
837            .enumerate()
838            .filter_map(|(idx, &v)| v.then_some(idx))
839            .collect::<Vec<_>>();
840        let rank_from_indices = Mask::from_indices(rank_len, rank_indices_vec);
841
842        let expected = expected_intersect_by_rank(&base_bits, &rank_bits);
843
844        assert_eq!(
845            base.intersect_by_rank(&rank_from_buffer),
846            expected,
847            "uncached rank"
848        );
849        assert_eq!(
850            base.intersect_by_rank(&rank_from_indices),
851            expected,
852            "cached rank"
853        );
854    }
855
856    #[rstest]
857    #[case::short(37, 0, 0)]
858    #[case::base_offset(257, 5, 0)]
859    #[case::rank_offset(257, 0, 3)]
860    #[case::both_offsets(513, 6, 5)]
861    fn test_intersect_by_rank_bitbuffer_paths_with_offsets(
862        #[case] base_len: usize,
863        #[case] base_offset: usize,
864        #[case] rank_offset: usize,
865    ) {
866        let base_source: Vec<bool> = (0..base_len + base_offset + 16)
867            .map(|i| (i % 3 == 0) ^ (i % 11 == 0) ^ (i % 17 == 0))
868            .collect();
869        let base_bits = base_source[base_offset..base_offset + base_len].to_vec();
870        let base = Mask::from_buffer(
871            BitBuffer::from(base_source).slice(base_offset..base_offset + base_len),
872        );
873
874        let rank_len = base.true_count();
875        let rank_bits: Vec<bool> = (0..rank_len)
876            .map(|i| (i % 5 == 0) || (i % 13 == 3))
877            .collect();
878        let mut rank_source = vec![false; rank_offset];
879        rank_source.extend(rank_bits.iter().copied());
880        rank_source.extend([true, false, true, false, true, false, true, false]);
881
882        let rank_from_buffer = Mask::from_buffer(
883            BitBuffer::from(rank_source).slice(rank_offset..rank_offset + rank_len),
884        );
885        let rank_indices = rank_bits
886            .iter()
887            .enumerate()
888            .filter_map(|(idx, &value)| value.then_some(idx))
889            .collect::<Vec<_>>();
890        let rank_from_indices = Mask::from_indices(rank_len, rank_indices);
891
892        let expected = expected_intersect_by_rank(&base_bits, &rank_bits);
893
894        assert_eq!(base.intersect_by_rank(&rank_from_buffer), expected);
895        assert_eq!(base.intersect_by_rank(&rank_from_indices), expected);
896    }
897
898    /// xorshift64, so the portable tests need no dependency and see the same words on every run.
899    fn xorshift(state: &mut u64) -> u64 {
900        *state ^= *state << 13;
901        *state ^= *state >> 7;
902        *state ^= *state << 17;
903        *state
904    }
905
906    fn deposit_reference(mut source: u64, mut mask: u64) -> u64 {
907        let mut result = 0u64;
908        while mask != 0 {
909            let bit = mask & mask.wrapping_neg();
910            if source & 1 != 0 {
911                result |= bit;
912            }
913            source >>= 1;
914            mask &= mask - 1;
915        }
916        result
917    }
918
919    #[test]
920    fn portable_deposit_matches_reference() {
921        deposit_matches_reference::<Portable>();
922        deposit_matches_reference::<PortableLoop>();
923    }
924
925    fn deposit_matches_reference<D: DepositBits>() {
926        let mut state = 0x9E37_79B9_7F4A_7C15;
927        for round in 0..200_000 {
928            // Up to six ANDs of random words, inverted every other round: all densities,
929            // down to masks with none or all of the bits.
930            let mut mask = xorshift(&mut state);
931            for _ in 0..round % 7 {
932                mask &= xorshift(&mut state);
933            }
934            if round % 2 == 1 {
935                mask = !mask;
936            }
937            let source = xorshift(&mut state);
938            let count = mask.count_ones() as usize;
939            assert_eq!(
940                D::deposit_bits(source, mask, count),
941                deposit_reference(source, mask),
942                "source {source:#018x}, mask {mask:#018x}"
943            );
944        }
945    }
946
947    #[cfg(target_arch = "aarch64")]
948    #[test]
949    fn sve2_bdep_matches_reference() {
950        if !std::arch::is_aarch64_feature_detected!("sve2-bitperm") {
951            return;
952        }
953        deposit_matches_reference::<Sve2Bdep>();
954        for offset in [0, 3, 67] {
955            kernels_match_reference::<Sve2Bdep>(offset);
956        }
957        let mut state = 0x9E37_79B9_7F4A_7C15;
958        for _ in 0..10_000 {
959            let word = xorshift(&mut state) | 1;
960            for rank in 0..word.count_ones() as usize {
961                let bit = deposit_reference(1u64 << rank, word).trailing_zeros() as usize;
962                assert_eq!(Sve2Bdep::select_bit_position(word, rank), bit);
963            }
964        }
965    }
966
967    /// The portable kernels directly, as dispatch picks BMI2 or SVE2 where it is available.
968    #[rstest]
969    #[case::aligned(0)]
970    #[case::offset(3)]
971    #[case::past_a_word(67)]
972    fn portable_kernels_match_reference(#[case] offset: usize) {
973        kernels_match_reference::<Portable>(offset);
974        kernels_match_reference::<PortableLoop>(offset);
975    }
976
977    /// Both bit-buffer kernels with deposit `D`. Runs of empty and full chunks sit between
978    /// random ones.
979    fn kernels_match_reference<D: DepositBits>(offset: usize) {
980        let mut state = 0x2545_F491_4F6C_DD1D;
981        let mut base_source = vec![false; offset];
982        for run in 0..48 {
983            let words = 1 + (xorshift(&mut state) % 20) as usize;
984            for _ in 0..words {
985                let word = match run % 3 {
986                    0 => 0,
987                    1 => u64::MAX,
988                    _ => xorshift(&mut state),
989                };
990                base_source.extend((0..64).map(|i| (word >> i) & 1 == 1));
991            }
992        }
993        base_source.extend([true, false, true, true, false]);
994        let base_len = base_source.len() - offset;
995        let base_bits = base_source[offset..].to_vec();
996        let base_buffer = BitBuffer::from(base_source).slice(offset..offset + base_len);
997
998        let rank_len = base_bits.iter().filter(|&&b| b).count();
999        let rank_bits: Vec<bool> = (0..rank_len)
1000            .map(|_| !xorshift(&mut state).is_multiple_of(3))
1001            .collect();
1002        let mut rank_source = vec![true; offset];
1003        rank_source.extend(&rank_bits);
1004        let rank_buffer = BitBuffer::from(rank_source).slice(offset..offset + rank_len);
1005        let rank_indices: Vec<usize> = rank_bits
1006            .iter()
1007            .enumerate()
1008            .filter_map(|(idx, &value)| value.then_some(idx))
1009            .collect();
1010
1011        let expected = expected_intersect_by_rank(&base_bits, &rank_bits);
1012
1013        assert_eq!(
1014            intersect_bit_buffers::<D>(&base_buffer, &rank_buffer, rank_indices.len()),
1015            expected
1016        );
1017        assert_eq!(
1018            intersect_bit_buffer_by_rank_indices::<D>(&base_buffer, &rank_indices),
1019            expected
1020        );
1021    }
1022
1023    fn expected_intersect_by_rank(base_bits: &[bool], rank_bits: &[bool]) -> Mask {
1024        let mut rank = 0usize;
1025        Mask::from_iter(base_bits.iter().map(|&is_set| {
1026            if is_set {
1027                let keep = rank_bits[rank];
1028                rank += 1;
1029                keep
1030            } else {
1031                false
1032            }
1033        }))
1034    }
1035}