|
3 | 3 |
|
4 | 4 | //! Buffer-level filter dispatch. |
5 | 5 | //! |
6 | | -//! Provides [`filter_buffer`] which filters a [`Buffer<T>`] by [`MaskValues`], attempting an |
7 | | -//! in-place filter when the buffer has exclusive ownership. |
| 6 | +//! Reuses suitable cached mask representations and otherwise filters directly from the bitmap, |
| 7 | +//! avoiding temporary index or range allocations for one-shot filters. |
| 8 | +
|
| 9 | +use std::mem::size_of; |
8 | 10 |
|
9 | 11 | use vortex_buffer::Buffer; |
10 | 12 | use vortex_mask::MaskValues; |
11 | 13 |
|
| 14 | +use crate::arrays::filter::execute::byte_compress; |
12 | 15 | use crate::arrays::filter::execute::slice; |
13 | 16 |
|
| 17 | +const CACHED_INDICES_MAX_DENSITY: f64 = 0.5; |
| 18 | +const IN_PLACE_MIN_DENSITY: f64 = 0.5; |
| 19 | +const MIN_SLICES_AVERAGE_RUN_LENGTH: usize = 8; |
| 20 | + |
14 | 21 | /// Filter a [`Buffer<T>`] by [`MaskValues`], returning a new buffer. |
15 | 22 | /// |
16 | | -/// This will attempt to filter in-place (via [`Buffer::try_into_mut`]) when the buffer has |
17 | | -/// exclusive ownership, avoiding an extra allocation. |
| 23 | +/// Dense uniquely owned buffers are compacted in place. Shared and sparse buffers allocate an |
| 24 | +/// exact-sized output and choose between cached indices, cached long ranges, bitmap iteration, |
| 25 | +/// and byte-compress based on the mask and element width. |
18 | 26 | pub(super) fn filter_buffer<T: Copy>(buffer: Buffer<T>, mask: &MaskValues) -> Buffer<T> { |
19 | | - match buffer.try_into_mut() { |
20 | | - Ok(mut buffer_mut) => { |
21 | | - let new_len = slice::filter_slice_mut_by_mask_values(buffer_mut.as_mut_slice(), mask); |
22 | | - buffer_mut.truncate(new_len); |
23 | | - buffer_mut.freeze() |
| 27 | + assert_eq!(buffer.len(), mask.len()); |
| 28 | + |
| 29 | + let buffer = if mask.density() >= IN_PLACE_MIN_DENSITY { |
| 30 | + match buffer.try_into_mut() { |
| 31 | + Ok(mut buffer_mut) => { |
| 32 | + let new_len = filter_slice_in_place(buffer_mut.as_mut_slice(), mask); |
| 33 | + buffer_mut.truncate(new_len); |
| 34 | + return buffer_mut.freeze(); |
| 35 | + } |
| 36 | + Err(buffer) => buffer, |
24 | 37 | } |
25 | | - // Otherwise, allocate a new buffer and fill it in. |
26 | | - Err(buffer) => slice::filter_slice_by_mask_values(buffer.as_slice(), mask), |
| 38 | + } else { |
| 39 | + buffer |
| 40 | + }; |
| 41 | + |
| 42 | + filter_slice(buffer.as_slice(), mask) |
| 43 | +} |
| 44 | + |
| 45 | +fn filter_slice<T: Copy>(values: &[T], mask: &MaskValues) -> Buffer<T> { |
| 46 | + if let Some(slices) = useful_cached_slices(mask) { |
| 47 | + return slice::filter_slice_by_slices(values, slices, mask.true_count()); |
| 48 | + } |
| 49 | + |
| 50 | + if mask.density() <= CACHED_INDICES_MAX_DENSITY |
| 51 | + && let Some(indices) = mask.cached_indices() |
| 52 | + { |
| 53 | + return slice::filter_slice_by_indices(values, indices); |
| 54 | + } |
| 55 | + |
| 56 | + if mask.density() >= byte_compress_density_threshold::<T>() { |
| 57 | + byte_compress::filter_buffer(values, mask) |
| 58 | + } else { |
| 59 | + slice::filter_slice_by_bitmap(values, mask) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +fn filter_slice_in_place<T: Copy>(values: &mut [T], mask: &MaskValues) -> usize { |
| 64 | + if let Some(slices) = useful_cached_slices(mask) { |
| 65 | + return slice::filter_slice_mut_by_slices(values, slices); |
| 66 | + } |
| 67 | + |
| 68 | + if mask.density() <= CACHED_INDICES_MAX_DENSITY |
| 69 | + && let Some(indices) = mask.cached_indices() |
| 70 | + { |
| 71 | + return slice::filter_slice_mut_by_indices(values, indices); |
| 72 | + } |
| 73 | + |
| 74 | + slice::filter_slice_mut_by_bitmap(values, mask) |
| 75 | +} |
| 76 | + |
| 77 | +fn useful_cached_slices(mask: &MaskValues) -> Option<&[(usize, usize)]> { |
| 78 | + mask.cached_slices().filter(|slices| { |
| 79 | + slices.len() == 1 || mask.true_count() / slices.len() >= MIN_SLICES_AVERAGE_RUN_LENGTH |
| 80 | + }) |
| 81 | +} |
| 82 | + |
| 83 | +fn byte_compress_density_threshold<T>() -> f64 { |
| 84 | + let width = size_of::<T>(); |
| 85 | + |
| 86 | + // The scalar byte-LUT has a later crossover on AArch64, where the word-at-a-time set-bit walk |
| 87 | + // is particularly efficient. Wider values also favor the word walk until all-set bytes become |
| 88 | + // common. Keep these cases covered by `benches/filter_fixed_width.rs`. |
| 89 | + if cfg!(target_arch = "aarch64") { |
| 90 | + return match width { |
| 91 | + 8 => 0.75, |
| 92 | + _ => 0.9, |
| 93 | + }; |
| 94 | + } |
| 95 | + |
| 96 | + match width { |
| 97 | + 1 => 0.0, |
| 98 | + 2 | 4 => 0.5, |
| 99 | + 8 => 0.75, |
| 100 | + _ => 0.875, |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +/// Materialize sparse indices when enough sibling arrays will reuse the same mask. |
| 105 | +pub(super) fn prepare_mask_for_reuse(mask: &MaskValues, consumers: usize) { |
| 106 | + if consumers <= 1 || mask.cached_indices().is_some() || mask.cached_slices().is_some() { |
| 107 | + return; |
| 108 | + } |
| 109 | + |
| 110 | + let density_threshold = if consumers >= 3 { 0.1 } else { 0.05 }; |
| 111 | + if mask.density() > density_threshold { |
| 112 | + return; |
27 | 113 | } |
| 114 | + |
| 115 | + let first = mask.bit_buffer().select(0); |
| 116 | + let last = mask.bit_buffer().select(mask.true_count() - 1); |
| 117 | + if first |
| 118 | + .zip(last) |
| 119 | + .is_some_and(|(first, last)| last - first + 1 == mask.true_count()) |
| 120 | + { |
| 121 | + return; |
| 122 | + } |
| 123 | + |
| 124 | + let _ = mask.indices(); |
28 | 125 | } |
29 | 126 |
|
30 | 127 | #[cfg(test)] |
31 | 128 | mod tests { |
| 129 | + use vortex_buffer::BitBuffer; |
32 | 130 | use vortex_buffer::BufferMut; |
33 | 131 | use vortex_buffer::buffer; |
34 | 132 | use vortex_mask::Mask; |
@@ -80,4 +178,83 @@ mod tests { |
80 | 178 | let result = filter_buffer(buf, mask_values(&mask)); |
81 | 179 | assert_eq!(result, buffer![1u32, 2, 3, 4, 6, 7, 8, 10]); |
82 | 180 | } |
| 181 | + |
| 182 | + #[test] |
| 183 | + fn test_filter_shared_buffer_by_cached_indices() { |
| 184 | + let buf = Buffer::from(BufferMut::from_iter(0u64..16)); |
| 185 | + let shared = buf.clone(); |
| 186 | + let mask = Mask::from_indices(16, [1, 5, 9, 15]); |
| 187 | + |
| 188 | + let result = filter_buffer(buf, mask_values(&mask)); |
| 189 | + assert_eq!(result, buffer![1u64, 5, 9, 15]); |
| 190 | + assert_eq!(shared.len(), 16); |
| 191 | + } |
| 192 | + |
| 193 | + #[test] |
| 194 | + fn test_filter_shared_buffer_by_cached_slices() { |
| 195 | + let buf = Buffer::from(BufferMut::from_iter(0u32..32)); |
| 196 | + let shared = buf.clone(); |
| 197 | + let mask = Mask::from_slices(32, vec![(3, 15), (20, 30)]); |
| 198 | + |
| 199 | + let result = filter_buffer(buf, mask_values(&mask)); |
| 200 | + let expected = (3u32..15).chain(20..30).collect::<Vec<_>>(); |
| 201 | + assert_eq!(result.as_slice(), expected.as_slice()); |
| 202 | + assert_eq!(shared.len(), 32); |
| 203 | + } |
| 204 | + |
| 205 | + #[test] |
| 206 | + fn test_filter_shared_dense_bitmap() { |
| 207 | + let buf = Buffer::from(BufferMut::from_iter(0u16..128)); |
| 208 | + let shared = buf.clone(); |
| 209 | + let mask = Mask::from_iter((0..128).map(|index| index != 63)); |
| 210 | + |
| 211 | + let result = filter_buffer(buf, mask_values(&mask)); |
| 212 | + let expected = (0u16..128).filter(|&value| value != 63).collect::<Vec<_>>(); |
| 213 | + assert_eq!(result.as_slice(), expected.as_slice()); |
| 214 | + assert_eq!(shared.len(), 128); |
| 215 | + } |
| 216 | + |
| 217 | + #[test] |
| 218 | + fn test_filter_unaligned_bitmap_words() { |
| 219 | + const LEN: usize = 151; |
| 220 | + const OFFSET: usize = 5; |
| 221 | + |
| 222 | + let backing = BitBuffer::from_iter( |
| 223 | + std::iter::repeat_n(false, OFFSET).chain((0..LEN).map(|index| index % 7 == 2)), |
| 224 | + ); |
| 225 | + let mask = Mask::from_buffer(BitBuffer::new_with_offset( |
| 226 | + backing.inner().clone(), |
| 227 | + LEN, |
| 228 | + OFFSET, |
| 229 | + )); |
| 230 | + let buf = Buffer::from(BufferMut::from_iter(0u64..LEN as u64)); |
| 231 | + |
| 232 | + let result = filter_buffer(buf, mask_values(&mask)); |
| 233 | + let expected = (0u64..LEN as u64) |
| 234 | + .filter(|value| value % 7 == 2) |
| 235 | + .collect::<Vec<_>>(); |
| 236 | + assert_eq!(result.as_slice(), expected.as_slice()); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn test_prepare_sparse_mask_for_sibling_reuse() { |
| 241 | + let two_consumers = Mask::from_iter((0..100).map(|index| index % 20 == 0)); |
| 242 | + let values = mask_values(&two_consumers); |
| 243 | + assert!(values.cached_indices().is_none()); |
| 244 | + prepare_mask_for_reuse(values, 2); |
| 245 | + assert!(values.cached_indices().is_some()); |
| 246 | + |
| 247 | + let three_consumers = Mask::from_iter((0..100).map(|index| index % 10 == 0)); |
| 248 | + let values = mask_values(&three_consumers); |
| 249 | + assert!(values.cached_indices().is_none()); |
| 250 | + prepare_mask_for_reuse(values, 2); |
| 251 | + assert!(values.cached_indices().is_none()); |
| 252 | + prepare_mask_for_reuse(values, 3); |
| 253 | + assert!(values.cached_indices().is_some()); |
| 254 | + |
| 255 | + let contiguous = Mask::from_iter((0..100).map(|index| (20..25).contains(&index))); |
| 256 | + let values = mask_values(&contiguous); |
| 257 | + prepare_mask_for_reuse(values, 3); |
| 258 | + assert!(values.cached_indices().is_none()); |
| 259 | + } |
83 | 260 | } |
0 commit comments