Skip to main content

simd_kernels/kernels/bitmask/
mod.rs

1// Copyright 2025 Peter Garfield Bower
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # **Bitmask Kernels Module** - *High-Performance Null-Aware Bitmask Operations*
16//!
17//! SIMD-optimised bitmask operations for Arrow-compatible nullable array processing with efficient null handling.
18//!
19//! ## Overview
20//!
21//! This module provides the foundational bitmask operations that enable null-aware and bit-packed boolean computing
22//! throughout the minarrow ecosystem, but can be applied to any bitmasking contenxt.
23//! These kernels handle bitwise logical operations, set membership tests, equality comparisons,
24//! and population counts on Arrow-format bitmasks with optimal performance characteristics.
25//!
26//! ## Architecture
27//!
28//! The bitmask module follows a three-tier architecture:
29//! - **Dispatch layer**: Smart runtime selection between SIMD and scalar implementations
30//! - **SIMD kernels**: Vectorised implementations using `std::simd` with portable lane counts
31//! - **Scalar kernels**: High-performance but non-SIMD fallback implementations for compatibility
32//!
33//! ## Modules
34//! - **`dispatch`**: Runtime dispatch layer selecting SIMD vs scalar implementations based on feature flags
35//! - **`simd`**: SIMD-accelerated implementations using vectorised bitwise operations with configurable lane counts
36//! - **`std`**: Scalar fallback implementations for word-level operations on 64-bit boundaries
37//!
38//! ## Core Operations
39//!
40//! ### **Logical Operations**
41//! - **`and_masks`**: Bitwise AND across two bitmasks for intersection operations
42//! - **`or_masks`**: Bitwise OR across two bitmasks for union operations  
43//! - **`xor_masks`**: Bitwise XOR across two bitmasks for symmetric difference
44//! - **`not_mask`**: Bitwise NOT for complement operations
45//!
46//! ### **Set Membership**
47//! - **`in_mask`**: Set inclusion tests - output bits indicate membership of LHS values in RHS set
48//! - **`not_in_mask`**: Set exclusion tests - complement of inclusion operations
49//!
50//! ### **Equality Testing**
51//! - **`eq_mask`**: Element-wise equality comparison producing result bitmask
52//! - **`ne_mask`**: Element-wise inequality comparison producing result bitmask
53//! - **`all_eq`**: Bulk equality test across entire bitmask windows
54//! - **`all_ne`**: Bulk inequality test across entire bitmask windows
55//!
56//! ### **Population Analysis**
57//! - **`popcount_mask`**: Fast population count (number of set bits) using SIMD reduction
58//! - **`all_true_mask`**: Test if all bits in bitmask are set to 1
59//! - **`all_false_mask`**: Test if all bits in bitmask are set to 0
60//!
61//! ## Arrow Compatibility
62//!
63//! All operations maintain full compatibility with Apache Arrow's bitmask format:
64//! - **LSB bit ordering**: Bit 0 is the least significant bit in each byte
65//! - **Byte-packed storage**: 8 bits per byte with proper alignment handling
66//! - **Trailing bit management**: Automatic masking of unused bits in final bytes
67//! - **64-bit word alignment**: Optimised for modern CPU architectures
68
69pub mod dispatch;
70#[cfg(feature = "simd")]
71pub mod simd;
72#[cfg(not(feature = "simd"))]
73pub mod std;
74
75use crate::{Bitmask, BitmaskVT};
76use core::mem;
77
78/// Fundamental word type for bitmask operations on 64-bit architectures.
79///
80/// Defines the basic unit of bitmask storage and manipulation. All bitmask
81/// operations are optimised around 64-bit word boundaries for maximum
82/// performance on modern CPU architectures.
83///
84/// # Architecture Alignment
85/// This type is chosen to align with:
86/// - 64-bit CPU registers: Native register width on x86-64 and AArch64
87/// - Cache line efficiency: Optimal memory access patterns
88/// - SIMD compatibility: Natural alignment for vectorised operations
89pub type Word = u64;
90
91/// Number of bits in a `Word` for bit-level bitmask calculations.
92///
93/// This constant determines the fundamental granularity of bitmask operations,
94/// enabling efficient bit manipulation algorithms that operate on word
95/// boundaries.
96pub const WORD_BITS: usize = mem::size_of::<Word>() * 8;
97
98/// Helper to compute number of u64 words required for bitmask of `len` bits.
99#[inline(always)]
100pub fn words_for(len: usize) -> usize {
101    (len + WORD_BITS - 1) / WORD_BITS
102}
103
104/// Cast &[u8] to &[u64] for word-wise access.
105#[inline(always)]
106pub unsafe fn mask_bits_as_words(bits: &[u8]) -> *const Word {
107    bits.as_ptr() as *const Word
108}
109
110/// Cast &mut [u8] to &mut [u64] for word-wise mutation.
111#[inline(always)]
112pub unsafe fn mask_bits_as_words_mut(bits: &mut [u8]) -> *mut Word {
113    bits.as_mut_ptr() as *mut Word
114}
115
116/// Create zeroed bitmask of length `len` bits.
117#[inline(always)]
118pub fn new_mask(len: usize) -> Bitmask {
119    Bitmask::new_set_all(len, false)
120}
121
122/// Return window into bitmask's bits slice covering offset..offset+len bits.
123#[inline(always)]
124pub fn bitmask_window_bytes(mask: &Bitmask, offset: usize, len: usize) -> &[u8] {
125    let start = offset / 8;
126    let end = (offset + len + 7) / 8;
127    &mask.bits[start..end]
128}
129
130/// Return mutable window into bitmask's bits slice covering offset..offset+len bits.
131/// Enables efficient in-place modification of bitmask regions.
132#[inline(always)]
133pub fn bitmask_window_bytes_mut(mask: &mut Bitmask, offset: usize, len: usize) -> &mut [u8] {
134    let start = offset / 8;
135    let end = (offset + len + 7) / 8;
136    &mut mask.bits[start..end]
137}
138
139/// Zero all slack bits ≥ `bm.len`.
140#[inline(always)]
141pub fn clear_trailing_bits(bm: &mut Bitmask) {
142    if bm.len == 0 {
143        return;
144    }
145    let used = bm.len & 7;
146    if used != 0 {
147        let last = bm.bits.last_mut().unwrap();
148        *last &= (1u8 << used) - 1;
149    }
150}
151
152/// Quick population count of true bits
153#[inline(always)]
154pub fn popcount_bits(m: BitmaskVT<'_>) -> usize {
155    #[cfg(feature = "simd")]
156    {
157        // Use a default SIMD width if not otherwise specified
158        use crate::kernels::bitmask::simd::popcount_mask_simd;
159        popcount_mask_simd::<8>(m)
160    }
161    #[cfg(not(feature = "simd"))]
162    {
163        use crate::kernels::bitmask::std::popcount_mask;
164        popcount_mask(m)
165    }
166}
167
168/// Merge two optional Bitmasks into a new output mask, computing per-row AND.
169/// Returns None if both inputs are None (output is dense).
170#[inline]
171pub fn merge_bitmasks_to_new(
172    lhs: Option<&Bitmask>,
173    rhs: Option<&Bitmask>,
174    len: usize,
175) -> Option<Bitmask> {
176    match (lhs, rhs) {
177        (None, None) => None,
178        (Some(l), None) | (None, Some(l)) => {
179            debug_assert!(l.len() >= len, "Bitmask too short in merge");
180            let mut out = Bitmask::new_set_all(len, true);
181            for i in 0..len {
182                out.set(i, l.get(i));
183            }
184            Some(out)
185        }
186        (Some(l), Some(r)) => {
187            debug_assert!(l.len() >= len, "Left Bitmask too short in merge");
188            debug_assert!(r.len() >= len, "Right Bitmask too short in merge");
189            let mut out = Bitmask::new_set_all(len, true);
190            for i in 0..len {
191                out.set(i, l.get(i) && r.get(i));
192            }
193            Some(out)
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::Bitmask;
202
203    #[test]
204    fn test_words_for() {
205        assert_eq!(words_for(0), 0);
206        assert_eq!(words_for(1), 1);
207        assert_eq!(words_for(63), 1);
208        assert_eq!(words_for(64), 1);
209        assert_eq!(words_for(65), 2);
210        assert_eq!(words_for(128), 2);
211        assert_eq!(words_for(129), 3);
212    }
213
214    #[test]
215    fn test_mask_bits_as_words_and_mut() {
216        // Test reading words from bits
217        let mut mask = Bitmask::new_set_all(128, false);
218        // Write known pattern into mask
219        mask.set(0, true);
220        mask.set(63, true);
221        mask.set(64, true);
222        mask.set(127, true);
223        let bits = &mask.bits;
224        unsafe {
225            let words = mask_bits_as_words(bits);
226            assert_eq!(*words, 1u64 | (1u64 << 63));
227            assert_eq!(*words.add(1), 1u64 | (1u64 << 63));
228        }
229
230        // Test writing via mask_bits_as_words_mut
231        let mut mask = Bitmask::new_set_all(128, false);
232        let bits = &mut mask.bits;
233        unsafe {
234            let words_mut = mask_bits_as_words_mut(bits);
235            *words_mut = 0xDEADBEEFDEADBEEF;
236            *words_mut.add(1) = 0xCAFEBABECAFEBABE;
237        }
238        assert_eq!(mask.bits[0], 0xEF);
239        assert_eq!(mask.bits[7], 0xDE);
240        assert_eq!(mask.bits[8], 0xBE);
241        assert_eq!(mask.bits[15], 0xCA);
242    }
243
244    #[test]
245    fn test_bitmask_window_bytes() {
246        let mut mask = Bitmask::new_set_all(24, false);
247        mask.set(0, true);
248        mask.set(7, true);
249        mask.set(8, true);
250        mask.set(15, true);
251        mask.set(16, true);
252        mask.set(23, true);
253        // Window: bytes 1..3 should cover bits 8..24
254        let bytes = bitmask_window_bytes(&mask, 8, 16);
255        assert_eq!(bytes.len(), 2); // 16 bits = 2 bytes
256        assert_eq!(bytes[0], 0b10000001); // bits 8 and 15 set
257        assert_eq!(bytes[1], 0b10000001); // bits 16 and 23 set
258    }
259
260    #[test]
261    fn test_bitmask_window_bytes_mut() {
262        let mut mask = Bitmask::new_set_all(16, false);
263        {
264            let window = bitmask_window_bytes_mut(&mut mask, 0, 16);
265            window[0] = 0xAA;
266            window[1] = 0x55;
267        }
268        assert_eq!(mask.bits[0], 0xAA);
269        assert_eq!(mask.bits[1], 0x55);
270    }
271
272    #[test]
273    fn test_clear_trailing_bits() {
274        let mut mask = Bitmask::new_set_all(10, true);
275        // The last byte should have only the low 2 bits set after clearing trailing bits
276        clear_trailing_bits(&mut mask);
277        // 10 bits => 2 bytes, last byte should have only bits 0 and 1 set (0b00000011 == 0x03)
278        assert_eq!(mask.bits[1], 0x03);
279        // The remaining bits should still be set
280        for i in 0..10 {
281            assert!(mask.get(i));
282        }
283        // Any bits beyond 10 should be cleared
284        for i in 10..16 {
285            assert!(!mask.get(i));
286        }
287    }
288}