Skip to main content

vortex_fastlanes/bitpacking/array/
unpack_iter.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::mem;
5use std::mem::MaybeUninit;
6use std::ops::Range;
7
8use fastlanes::BitPacking;
9use lending_iterator::gat;
10use lending_iterator::prelude::Item;
11#[gat(Item)]
12use lending_iterator::prelude::LendingIterator;
13use vortex_array::dtype::PhysicalPType;
14use vortex_error::VortexResult;
15use vortex_error::vortex_ensure;
16
17use crate::BitPackedData;
18use crate::FL_CHUNK_SIZE;
19
20const CHUNK_SIZE: usize = FL_CHUNK_SIZE;
21
22/// Strategy trait for fastlanes unpacking operations
23pub trait UnpackStrategy<T: PhysicalPType> {
24    /// Unpack a chunk of packed data into the destination buffer
25    ///
26    /// # Safety
27    /// - `chunk` must contain exactly `elems_per_chunk` elements
28    /// - `dst` must have exactly CHUNK_SIZE capacity
29    unsafe fn unpack_chunk(&self, bit_width: usize, chunk: &[T::Physical], dst: &mut [T::Physical]);
30}
31
32/// BitPacking strategy - uses plain bitpacking without reference value
33pub struct BitPackingStrategy;
34
35impl<T: PhysicalPType<Physical: BitPacking>> UnpackStrategy<T> for BitPackingStrategy {
36    #[allow(clippy::inline_always)]
37    #[inline(always)]
38    unsafe fn unpack_chunk(
39        &self,
40        bit_width: usize,
41        chunk: &[T::Physical],
42        dst: &mut [T::Physical],
43    ) {
44        // SAFETY: Caller must ensure [`BitPacking::unchecked_unpack`] safety requirements hold.
45        unsafe {
46            BitPacking::unchecked_unpack(bit_width, chunk, dst);
47        }
48    }
49}
50
51/// Accessor to unpacked chunks of bitpacked arrays
52///
53/// The usual pattern of usage should follow
54/// ```
55/// use std::mem::MaybeUninit;
56///
57/// use lending_iterator::gat;
58/// use lending_iterator::prelude::Item;
59/// #[gat(Item)]
60/// use lending_iterator::prelude::LendingIterator;
61/// use vortex_array::IntoArray;
62/// use vortex_array::VortexSessionExecute;
63/// use vortex_buffer::buffer;
64/// use vortex_fastlanes::BitPackedData;
65/// use vortex_fastlanes::BitPackedArrayExt;
66/// use vortex_fastlanes::unpack_iter::BitUnpackedChunks;
67///
68/// let mut ctx = vortex_array::array_session().create_execution_ctx();
69/// let array = BitPackedData::encode(&buffer![2, 3, 4, 5].into_array(), 2, &mut ctx).unwrap();
70/// let mut scratch = [const { MaybeUninit::<i32>::uninit() }; 1024];
71/// let mut unpacked_chunks: BitUnpackedChunks<i32> =
72///     array.unpacked_chunks(&mut scratch).unwrap();
73///
74/// if let Some(header) = unpacked_chunks.initial() {
75///    // handle partial initial chunk
76/// }
77///
78/// {
79///     let mut chunks_iter = unpacked_chunks.full_chunks();
80///     while let Some(chunk) = chunks_iter.next() {
81///         // handle full bitpacked chunks of 1024 elements
82///     }
83/// }
84///
85/// if let Some(trailer) = unpacked_chunks.trailer() {
86///     // handle partial trailing chunk
87/// }
88/// ```
89pub struct UnpackedChunks<'a, T: PhysicalPType, S: UnpackStrategy<T>> {
90    strategy: S,
91    bit_width: usize,
92    offset: usize,
93    len: usize,
94    num_chunks: usize,
95    // 0 indicates full chunk of CHUNK_SIZE
96    last_chunk_length: usize,
97    packed: &'a [T::Physical],
98    scratch: &'a mut [MaybeUninit<T>; CHUNK_SIZE],
99}
100
101pub type BitUnpackedChunks<'a, T> = UnpackedChunks<'a, T, BitPackingStrategy>;
102
103impl<'a, T: BitPacked> BitUnpackedChunks<'a, T> {
104    pub fn try_new(
105        array: &'a BitPackedData,
106        len: usize,
107        scratch: &'a mut [MaybeUninit<T>; CHUNK_SIZE],
108    ) -> VortexResult<Self> {
109        Self::try_new_with_strategy(
110            BitPackingStrategy,
111            array.packed_slice::<T::Physical>(),
112            array.bit_width() as usize,
113            array.offset() as usize,
114            len,
115            scratch,
116        )
117    }
118
119    pub fn full_chunks(&mut self) -> BitUnpackIterator<'_, T> {
120        let elems_per_chunk = self.elems_per_chunk();
121        let last_chunk_is_sliced = self.last_chunk_is_sliced() as usize;
122        let first_chunk_is_sliced = self.first_chunk_is_sliced();
123        BitUnpackIterator::new(
124            self.packed,
125            self.scratch,
126            self.bit_width,
127            elems_per_chunk,
128            self.num_chunks - last_chunk_is_sliced,
129            first_chunk_is_sliced,
130        )
131    }
132}
133
134impl<'a, T: PhysicalPType, S: UnpackStrategy<T>> UnpackedChunks<'a, T, S> {
135    pub fn try_new_with_strategy(
136        strategy: S,
137        packed: &'a [T::Physical],
138        bit_width: usize,
139        offset: usize,
140        len: usize,
141        scratch: &'a mut [MaybeUninit<T>; CHUNK_SIZE],
142    ) -> VortexResult<Self> {
143        let (num_chunks, last_chunk_length) =
144            validate_packed::<T>(packed.len(), bit_width, offset, len)?;
145        Ok(Self {
146            strategy,
147            bit_width,
148            offset,
149            len,
150            num_chunks,
151            last_chunk_length,
152            packed,
153            scratch,
154        })
155    }
156
157    #[allow(clippy::inline_always)]
158    #[inline(always)]
159    const fn elems_per_chunk(&self) -> usize {
160        128 * self.bit_width / size_of::<T>()
161    }
162
163    /// Access first chunk of the array if the last chunk has fewer than 1024 due to slicing
164    pub fn initial(&mut self) -> Option<&mut [T]> {
165        (self.first_chunk_is_sliced() || self.num_chunks == 1).then(|| {
166            let chunk: &[T::Physical] = &self.packed[..self.elems_per_chunk()];
167            let dst: &mut [MaybeUninit<T>] = self.scratch;
168            let dst: &mut [T::Physical] = unsafe { mem::transmute(dst) };
169
170            let header_end_slice = if self.num_chunks == 1 {
171                self.len
172            } else {
173                CHUNK_SIZE - self.offset
174            };
175            // SAFETY:
176            // 1. chunk is elems_per_chunk.
177            // 2. buffer is exactly CHUNK_SIZE.
178            unsafe {
179                self.strategy.unpack_chunk(self.bit_width, chunk, dst);
180                mem::transmute(&mut self.scratch[self.offset..][..header_end_slice])
181            }
182        })
183    }
184
185    /// Decode all chunks (initial, full, and trailer) directly into the output range.
186    pub fn decode_into(&mut self, output: &mut [MaybeUninit<T>]) {
187        debug_assert_eq!(output.len(), self.len);
188        let mut local_idx = 0;
189
190        if let Some(initial) = self.initial() {
191            local_idx = initial.len();
192
193            // TODO(connor): use maybe_uninit_write_slice when it gets stabilized.
194            // SAFETY: &[T] and &[MaybeUninit<T>] have the same layout.
195            let init_initial: &[MaybeUninit<T>] = unsafe { mem::transmute(initial) };
196            output[..local_idx].copy_from_slice(init_initial);
197        }
198
199        local_idx = self.decode_full_chunks_into_at(output, local_idx);
200
201        if let Some(trailer) = self.trailer() {
202            // TODO(connor): use maybe_uninit_write_slice when it gets stabilized.
203            // SAFETY: &[T] and &[MaybeUninit<T>] have the same layout.
204            let init_trailer: &[MaybeUninit<T>] = unsafe { mem::transmute(trailer) };
205            output[local_idx..][..init_trailer.len()].copy_from_slice(init_trailer);
206            local_idx += init_trailer.len();
207        }
208
209        debug_assert_eq!(local_idx, self.len);
210    }
211
212    /// Decode all chunks (initial, full, and trailer), mapping each unpacked value through f.
213    pub(crate) fn decode_map_into<U>(
214        &mut self,
215        output: &mut [MaybeUninit<U>],
216        mut f: impl FnMut(T) -> U,
217    ) {
218        debug_assert_eq!(output.len(), self.len);
219
220        self.for_each_unpacked_chunk(|chunk, range| {
221            write_map(chunk, &mut output[range], &mut f);
222        });
223    }
224
225    /// Walk every unpacked chunk in array order, reusing the internal scratch buffer.
226    pub(crate) fn for_each_unpacked_chunk<F>(&mut self, mut f: F)
227    where
228        F: FnMut(&mut [T], Range<usize>),
229    {
230        let mut local_idx = 0;
231
232        if let Some(initial) = self.initial() {
233            let chunk_len = initial.len();
234            f(initial, local_idx..local_idx + chunk_len);
235            local_idx += chunk_len;
236        }
237
238        if self.num_chunks > 1 {
239            let packed_slice = self.packed;
240            let elems_per_chunk = self.elems_per_chunk();
241            for i in self.full_chunks_range() {
242                let chunk = &packed_slice[i * elems_per_chunk..][..elems_per_chunk];
243                unsafe {
244                    let dst: &mut [T::Physical] = mem::transmute(&mut self.scratch[..]);
245                    self.strategy.unpack_chunk(self.bit_width, chunk, dst);
246                    let unpacked: &mut [T] = mem::transmute(&mut self.scratch[..]);
247                    f(unpacked, local_idx..local_idx + CHUNK_SIZE);
248                }
249                local_idx += CHUNK_SIZE;
250            }
251        }
252
253        if let Some(trailer) = self.trailer() {
254            let chunk_len = trailer.len();
255            f(trailer, local_idx..local_idx + chunk_len);
256            local_idx += chunk_len;
257        }
258
259        debug_assert_eq!(local_idx, self.len);
260    }
261
262    /// Unpack full chunks into output range starting at the given index.
263    fn decode_full_chunks_into_at(
264        &mut self,
265        output: &mut [MaybeUninit<T>],
266        start_idx: usize,
267    ) -> usize {
268        if self.num_chunks == 1 {
269            return start_idx;
270        }
271
272        let mut local_idx = start_idx;
273
274        let packed_slice = self.packed;
275        let elems_per_chunk = self.elems_per_chunk();
276        for i in self.full_chunks_range() {
277            let chunk = &packed_slice[i * elems_per_chunk..][..elems_per_chunk];
278
279            unsafe {
280                let uninit_dst = &mut output[local_idx..local_idx + CHUNK_SIZE];
281                // SAFETY: &[T] and &[MaybeUninit<T>] have the same layout.
282                let dst: &mut [T::Physical] = mem::transmute(uninit_dst);
283                self.strategy.unpack_chunk(self.bit_width, chunk, dst);
284            }
285            local_idx += CHUNK_SIZE;
286        }
287        local_idx
288    }
289
290    fn full_chunks_range(&self) -> Range<usize> {
291        (self.first_chunk_is_sliced() as usize)
292            ..(self.num_chunks - self.last_chunk_is_sliced() as usize)
293    }
294
295    /// Access last chunk of the array if the last chunk has fewer than 1024 due to slicing
296    pub fn trailer(&mut self) -> Option<&mut [T]> {
297        (self.last_chunk_is_sliced() && self.num_chunks > 1).then(|| {
298            let chunk: &[T::Physical] = &self.packed
299                [(self.num_chunks - 1) * self.elems_per_chunk()..][..self.elems_per_chunk()];
300            let dst: &mut [MaybeUninit<T>] = self.scratch;
301            let dst: &mut [T::Physical] = unsafe { mem::transmute(dst) };
302            // SAFETY:
303            // 1. chunk is elems_per_chunk.
304            // 2. buffer is exactly CHUNK_SIZE.
305            unsafe {
306                self.strategy.unpack_chunk(self.bit_width, chunk, dst);
307                mem::transmute(&mut self.scratch[..self.last_chunk_length])
308            }
309        })
310    }
311
312    fn last_chunk_is_sliced(&self) -> bool {
313        self.last_chunk_length != 0
314    }
315
316    fn first_chunk_is_sliced(&self) -> bool {
317        self.offset != 0
318    }
319}
320
321/// Walk every packed chunk in array order without allocating an unpack scratch buffer.
322pub(crate) fn for_each_packed_chunk<T, F>(
323    packed: &[T::Physical],
324    bit_width: usize,
325    offset: usize,
326    len: usize,
327    mut f: F,
328) -> VortexResult<()>
329where
330    T: PhysicalPType,
331    F: FnMut(&[T::Physical], Range<usize>),
332{
333    let (num_chunks, _) = validate_packed::<T>(packed.len(), bit_width, offset, len)?;
334    let elems_per_chunk = 128 * bit_width / size_of::<T>();
335    let padded_len = offset + len;
336    for chunk in 0..num_chunks {
337        let packed_chunk = &packed[chunk * elems_per_chunk..][..elems_per_chunk];
338        let start = chunk * CHUNK_SIZE;
339        let end = (start + CHUNK_SIZE).min(padded_len);
340        f(packed_chunk, start..end);
341    }
342    Ok(())
343}
344
345fn validate_packed<T: PhysicalPType>(
346    packed_len: usize,
347    bit_width: usize,
348    offset: usize,
349    len: usize,
350) -> VortexResult<(usize, usize)> {
351    vortex_ensure!(
352        offset < CHUNK_SIZE,
353        "Invalid bit-packed offset {offset}, expected < {CHUNK_SIZE}"
354    );
355    let elems_per_chunk = 128 * bit_width / size_of::<T>();
356    let num_chunks = (offset + len).div_ceil(CHUNK_SIZE);
357    vortex_ensure!(
358        packed_len == num_chunks * elems_per_chunk,
359        "Invalid packed length: got {packed_len}, expected {}",
360        num_chunks * elems_per_chunk
361    );
362    Ok((num_chunks, (offset + len) % CHUNK_SIZE))
363}
364
365/// Iterator over full chunks of bitpacked array that yields unpacked chunks one at a time
366pub struct BitUnpackIterator<'a, T: BitPacked + 'a> {
367    packed: &'a [T::Physical],
368    buffer: &'a mut [MaybeUninit<T>; CHUNK_SIZE],
369    bit_width: usize,
370    elems_per_chunk: usize,
371    num_chunks: usize,
372    idx: usize,
373}
374
375impl<'a, T: BitPacked> BitUnpackIterator<'a, T> {
376    pub fn new(
377        packed: &'a [T::Physical],
378        buffer: &'a mut [MaybeUninit<T>; CHUNK_SIZE],
379        bit_width: usize,
380        elems_per_chunk: usize,
381        num_chunks: usize,
382        first_chunk_is_sliced: bool,
383    ) -> Self {
384        Self {
385            packed,
386            buffer,
387            bit_width,
388            elems_per_chunk,
389            num_chunks,
390            idx: if first_chunk_is_sliced { 1 } else { 0 },
391        }
392    }
393}
394
395#[gat]
396impl<'a, T: BitPacked + 'a> LendingIterator for BitUnpackIterator<'a, T> {
397    type Item<'next>
398    where
399        Self: 'next,
400    = &'next mut [T; CHUNK_SIZE];
401
402    fn next(&'_ mut self) -> Option<Item<'_, Self>> {
403        if self.idx >= self.num_chunks {
404            return None;
405        }
406
407        let chunk = &self.packed[self.idx * self.elems_per_chunk..][..self.elems_per_chunk];
408
409        let dst: &mut [MaybeUninit<T>] = self.buffer;
410        unsafe {
411            let dst: &mut [T::Physical] = mem::transmute(dst);
412
413            BitPacking::unchecked_unpack(self.bit_width, chunk, dst);
414        }
415        self.idx += 1;
416        // SAFETY: The buffer has the appropriate lifetime, the iterator signature doesn't account for it
417        Some(unsafe { mem::transmute::<&mut [MaybeUninit<T>; 1024], &mut [T; 1024]>(self.buffer) })
418    }
419}
420
421fn write_map<T: Copy, U>(src: &[T], dst: &mut [MaybeUninit<U>], f: &mut impl FnMut(T) -> U) {
422    for (dst, &src) in dst.iter_mut().zip(src.iter()) {
423        dst.write(f(src));
424    }
425}
426
427pub trait BitPacked: PhysicalPType<Physical: BitPacking> {}
428
429impl BitPacked for i8 {}
430impl BitPacked for i16 {}
431impl BitPacked for i32 {}
432impl BitPacked for i64 {}
433impl BitPacked for u8 {}
434impl BitPacked for u16 {}
435impl BitPacked for u32 {}
436impl BitPacked for u64 {}