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}