From 1d66d12fbd36ce097da5298d8935b00c4cb21d4f Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Tue, 29 Sep 2026 22:13:57 -0700 Subject: [PATCH 1/3] Optimise Patches::filter to better handle sparse masks and sparse patches --- vortex-array/src/patches.rs | 398 ++++++++++++++++++++++++++++-------- vortex-mask/src/lib.rs | 2 +- 2 files changed, 316 insertions(+), 84 deletions(-) diff --git a/vortex-array/src/patches.rs b/vortex-array/src/patches.rs index 47662e36fe1..73b65c754df 100644 --- a/vortex-array/src/patches.rs +++ b/vortex-array/src/patches.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::cmp::Ordering; use std::fmt::Debug; use std::hash::Hash; use std::ops::Range; @@ -18,6 +17,7 @@ use vortex_error::vortex_ensure; use vortex_error::vortex_err; use vortex_mask::AllOr; use vortex_mask::Mask; +use vortex_mask::MaskValues; use vortex_utils::aliases::hash_map::HashMap; use crate::ArrayRef; @@ -638,17 +638,17 @@ impl Patches { ); } - match mask.indices() { - AllOr::All => Ok(Some(self.clone())), - AllOr::None => Ok(None), - AllOr::Some(mask_indices) => { + match mask { + Mask::AllTrue(_) => Ok(Some(self.clone())), + Mask::AllFalse(_) => Ok(None), + Mask::Values(mask_values) => { let flat_indices = self.indices().clone().execute::(ctx)?; match_each_unsigned_integer_ptype!(flat_indices.ptype(), |I| { filter_patches_with_mask( flat_indices.as_slice::(), self.offset(), self.values(), - mask_indices, + mask_values, ctx.allocator(), ) }) @@ -1101,99 +1101,216 @@ where /// The filter mask may contain indices that are non-patched. The return value of this function /// is a new set of `Patches` with the indices relative to the provided `mask` rank, and the /// patch values. +/// +/// We search for each element of the smaller side in the larger side, so the cost is +/// logarithmic in the size of the larger side. We never build the mask indices here: +/// +/// * More patches than mask indices: gallop through the patches for each mask index. The mask +/// indices come from the mask cache, or from a walk over the set bits of the mask bitmap. +/// * Otherwise, with cached mask indices: gallop through the mask indices for each patch. +/// * Otherwise: look up each patch in the mask bitmap and count the set bits before it. fn filter_patches_with_mask( patch_indices: &[T], offset: usize, patch_values: &ArrayRef, - mask_indices: &[usize], + mask: &MaskValues, allocator: &BufferAllocatorRef, ) -> VortexResult> { - let true_count = mask_indices.len(); - let mut new_patch_indices = BufferMut::::with_capacity_in(true_count, allocator.clone()); - let mut new_mask_indices = Vec::with_capacity(true_count); - - // Attempt to move the window by `STRIDE` elements on each iteration. This assumes that - // the patches are relatively sparse compared to the overall mask, and so many indices in the - // mask will end up being skipped. - const STRIDE: usize = 4; - - let mut mask_idx = 0usize; - let mut true_idx = 0usize; - - while mask_idx < patch_indices.len() && true_idx < true_count { - // NOTE: we are searching for overlaps between sorted, unaligned indices in `patch_indices` - // and `mask_indices`. We assume that Patches are sparse relative to the global space of - // the mask (which covers both patch and non-patch values of the parent array), and so to - // quickly jump through regions with no overlap, we attempt to move our pointers by STRIDE - // elements on each iteration. If we cannot rule out overlap due to min/max values, we - // fallback to performing a two-way iterator merge. - if (mask_idx + STRIDE) < patch_indices.len() && (true_idx + STRIDE) < mask_indices.len() { - // Load a vector of each into our registers. - let left_min = patch_indices[mask_idx] - .to_usize() - .ok_or_else(|| vortex_err!("patch index does not fit in usize"))? - - offset; - let left_max = patch_indices[mask_idx + STRIDE] - .to_usize() - .ok_or_else(|| vortex_err!("patch index does not fit in usize"))? - - offset; - let right_min = mask_indices[true_idx]; - let right_max = mask_indices[true_idx + STRIDE]; - - if left_min > right_max { - // Advance right side - true_idx += STRIDE; - continue; - } else if right_min > left_max { - mask_idx += STRIDE; - continue; - } else { - // Fallthrough to direct comparison path. - } + let true_count = mask.true_count(); + let patch_count = patch_indices.len(); + let expected_matches = true_count.min(patch_count); + + // The position of each kept patch in the mask rank, and in the original patches. + let mut new_patch_indices = + BufferMut::::with_capacity_in(expected_matches, allocator.clone()); + let mut kept_patches = Vec::with_capacity(expected_matches); + + if patch_count > true_count { + let mut gallop = PatchGallop { + patch_indices, + offset, + patch_position: 0, + mask_position: 0, + new_patch_indices: &mut new_patch_indices, + kept_patches: &mut kept_patches, + }; + + // `NoMoreMatches` only stops the search early, so the result is not an error. + let _ = match mask.cached_indices() { + Some(mask_indices) => mask_indices + .iter() + .try_for_each(|&mask_index| gallop.next_mask_index(mask_index)), + None => mask + .bit_buffer() + .try_for_each_set_index(|mask_index| gallop.next_mask_index(mask_index)), + }; + } else { + match mask.cached_indices() { + Some(mask_indices) => gallop_mask_indices_for_patches( + patch_indices, + offset, + mask_indices, + &mut new_patch_indices, + &mut kept_patches, + )?, + None => rank_patches_in_mask_bitmap( + patch_indices, + offset, + mask.bit_buffer(), + &mut new_patch_indices, + &mut kept_patches, + )?, } + } - // Two-way sorted iterator merge: + if kept_patches.is_empty() { + return Ok(None); + } - let left = patch_indices[mask_idx] - .to_usize() - .ok_or_else(|| vortex_err!("patch index does not fit in usize"))? - - offset; - let right = mask_indices[true_idx]; + let new_patch_indices = new_patch_indices.into_array(); + let new_patch_values = + patch_values.filter(Mask::from_indices(patch_values.len(), kept_patches))?; + + // SAFETY: each kept patch adds one index and one value, so their lengths are equal. The + // indices are non-nullable `u64` positions in the mask rank. They are strictly increasing + // because the mask indices are, and each one is smaller than `true_count`. + Ok(Some(unsafe { + Patches::new_unchecked( + true_count, + 0, + new_patch_indices, + new_patch_values, + // TODO(0ax1): Chunk offsets are invalid after a filter is applied. + None, + None, + ) + })) +} - match left.cmp(&right) { - Ordering::Less => { - mask_idx += 1; - } - Ordering::Greater => { - true_idx += 1; - } - Ordering::Equal => { - // Save the mask index as well as the positional index. - new_mask_indices.push(mask_idx); - new_patch_indices.push(true_idx as u64); +/// Returns the first position at or after `start` whose value is not less than `needle`. +/// +/// The search doubles its step until it passes `needle`, then does a binary search in the last +/// step. The cost is logarithmic in the distance from `start` to the result, which makes a +/// sequence of searches with increasing needles cheap when the needles are sparse. +fn gallop_lower_bound(values: &[T], start: usize, needle: &T) -> usize { + let mut low = start; + let mut high = start; + let mut step = 1; + + while high < values.len() && values[high] < *needle { + low = high + 1; + high += step; + step *= 2; + } + + let high = high.min(values.len()); + low + values[low..high].partition_point(|value| value < needle) +} - mask_idx += 1; - true_idx += 1; - } +/// No later mask index can match a patch. +struct NoMoreMatches; + +/// Finds the patches for a sorted sequence of mask indices, for masks that are sparser than +/// the patches. +/// +/// The search compares values in the patch index type, so the patches do not need conversion. +struct PatchGallop<'a, T> { + patch_indices: &'a [T], + offset: usize, + patch_position: usize, + mask_position: usize, + new_patch_indices: &'a mut BufferMut, + kept_patches: &'a mut Vec, +} + +impl PatchGallop<'_, T> { + fn next_mask_index(&mut self, mask_index: usize) -> Result<(), NoMoreMatches> { + // Mask indices are sorted, so no later mask index fits in `T` either. + let needle = mask_index + .checked_add(self.offset) + .and_then(::from) + .ok_or(NoMoreMatches)?; + + self.patch_position = gallop_lower_bound(self.patch_indices, self.patch_position, &needle); + if self.patch_position == self.patch_indices.len() { + return Err(NoMoreMatches); } + + if self.patch_indices[self.patch_position] == needle { + self.new_patch_indices.push(self.mask_position as u64); + self.kept_patches.push(self.patch_position); + self.patch_position += 1; + } + + self.mask_position += 1; + Ok(()) } +} - if new_mask_indices.is_empty() { - return Ok(None); +/// Finds the mask position of each patch, for patches that are not sparser than the mask. +fn gallop_mask_indices_for_patches( + patch_indices: &[T], + offset: usize, + mask_indices: &[usize], + new_patch_indices: &mut BufferMut, + kept_patches: &mut Vec, +) -> VortexResult<()> { + let mut mask_position = 0; + + for (patch_position, patch_index) in patch_indices.iter().enumerate() { + let index = patch_index_to_usize(*patch_index, offset)?; + + mask_position = gallop_lower_bound(mask_indices, mask_position, &index); + if mask_position == mask_indices.len() { + break; + } + + if mask_indices[mask_position] == index { + new_patch_indices.push(mask_position as u64); + kept_patches.push(patch_position); + mask_position += 1; + } } - let new_patch_indices = new_patch_indices.into_array(); - let new_patch_values = - patch_values.filter(Mask::from_indices(patch_values.len(), new_mask_indices))?; - - Ok(Some(Patches::new( - true_count, - 0, - new_patch_indices, - new_patch_values, - // TODO(0ax1): Chunk offsets are invalid after a filter is applied. - None, - )?)) + Ok(()) +} + +/// Finds the mask position of each patch from the mask bitmap, for patches that are not +/// sparser than the mask. This does not create the mask indices. +/// +/// The mask position of a kept patch is the number of set bits before it. We count only the bits +/// between two adjacent patches, so the total count work is one pass over the mask words. +fn rank_patches_in_mask_bitmap( + patch_indices: &[T], + offset: usize, + mask: &BitBuffer, + new_patch_indices: &mut BufferMut, + kept_patches: &mut Vec, +) -> VortexResult<()> { + let mut rank = 0; + let mut previous_index = 0; + + for (patch_position, patch_index) in patch_indices.iter().enumerate() { + let index = patch_index_to_usize(*patch_index, offset)?; + + rank += mask.count_range(previous_index, index); + previous_index = index; + + if mask.value(index) { + new_patch_indices.push(rank as u64); + kept_patches.push(patch_position); + } + } + + Ok(()) +} + +#[inline] +fn patch_index_to_usize(patch_index: T, offset: usize) -> VortexResult { + Ok(patch_index + .to_usize() + .ok_or_else(|| vortex_err!("patch index does not fit in usize"))? + - offset) } fn take_indices_with_search_fn< @@ -1237,8 +1354,13 @@ fn take_indices_with_search_fn< #[cfg(test)] mod test { + use rstest::rstest; + use vortex_buffer::BitBuffer; + use vortex_buffer::Buffer; use vortex_buffer::BufferMut; use vortex_buffer::buffer; + use vortex_error::VortexExpect; + use vortex_error::VortexResult; use vortex_mask::Mask; use crate::IntoArray; @@ -1683,6 +1805,116 @@ mod test { ); } + /// Covers each filter algorithm: gallop over the patches with cached mask indices or with the + /// mask bitmap, gallop over cached mask indices, and rank in the mask bitmap. + #[rstest] + #[case::sparse_mask((0..1000).step_by(2).collect(), (0..1000).step_by(97).collect())] + #[case::sparse_patches((0..1000).step_by(97).collect(), (0..1000).step_by(2).collect())] + #[case::similar_sizes((0..1000).step_by(3).collect(), (0..1000).step_by(2).collect())] + #[case::no_overlap((0..500).collect(), (500..1000).step_by(50).collect())] + #[case::no_overlap_sparse_patches((500..1000).step_by(100).collect(), (0..500).collect())] + #[case::edges(vec![0, 1, 998, 999], vec![0, 999])] + fn test_filter_matches_naive( + #[case] patch_indices: Vec, + #[case] mask_indices: Vec, + #[values(0, 5)] offset: usize, + #[values(true, false)] cache_mask_indices: bool, + ) -> VortexResult<()> { + const LEN: usize = 1000; + let mut ctx = array_session().create_execution_ctx(); + + let patches = Patches::new( + LEN, + offset, + patch_indices + .iter() + .map(|&index| u32::try_from(index + offset).vortex_expect("index fits in u32")) + .collect::>() + .into_array(), + (0i32..) + .take(patch_indices.len()) + .collect::>() + .into_array(), + None, + )?; + + let mask = if cache_mask_indices { + Mask::from_indices(LEN, mask_indices.iter().copied()) + } else { + Mask::from_buffer(BitBuffer::from_iter( + (0..LEN).map(|index| mask_indices.binary_search(&index).is_ok()), + )) + }; + + let (expected_indices, expected_values): (Vec, Vec) = mask_indices + .iter() + .enumerate() + .filter_map(|(mask_position, index)| { + patch_indices + .binary_search(index) + .ok() + .map(|patch_position| { + ( + mask_position as u64, + i32::try_from(patch_position).vortex_expect("position fits in i32"), + ) + }) + }) + .unzip(); + + let filtered = patches.filter(&mask, &mut ctx)?; + + let Some(filtered) = filtered else { + assert!(expected_indices.is_empty()); + return Ok(()); + }; + + assert_eq!(filtered.array_len(), mask_indices.len()); + assert_arrays_eq!( + filtered.indices(), + PrimitiveArray::from_iter(expected_indices), + &mut ctx + ); + assert_arrays_eq!( + filtered.values(), + PrimitiveArray::from_iter(expected_values), + &mut ctx + ); + + Ok(()) + } + + /// Mask indices past the maximum of the patch index type cannot match a patch. + #[test] + fn test_filter_sparse_mask_past_index_type_max() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let patches = Patches::new( + 1000, + 0, + (0..=u8::MAX).collect::>().into_array(), + (0..256i32).collect::>().into_array(), + None, + )?; + + let mask = Mask::from_indices(1000, [3, 255, 256, 900]); + let filtered = patches + .filter(&mask, &mut ctx)? + .expect("patches at 3 and 255 are kept"); + + assert_arrays_eq!( + filtered.indices(), + PrimitiveArray::from_iter([0u64, 1]), + &mut ctx + ); + assert_arrays_eq!( + filtered.values(), + PrimitiveArray::from_iter([3i32, 255]), + &mut ctx + ); + + Ok(()) + } + #[test] fn test_filter_keep_all() { let mut ctx = array_session().create_execution_ctx(); diff --git a/vortex-mask/src/lib.rs b/vortex-mask/src/lib.rs index ac680289ac1..1fecee9a1d3 100644 --- a/vortex-mask/src/lib.rs +++ b/vortex-mask/src/lib.rs @@ -219,7 +219,7 @@ impl Mask { ); let buffer = BitBuffer::from_indices(len, indices.iter().copied()); debug_assert_eq!(buffer.len(), len); - let true_count = buffer.true_count(); + let true_count = indices.len(); if true_count == 0 { return Self::AllFalse(len); From 712d7965b1900979e7c0be8bcd4e68b143a9b995 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 30 Sep 2026 18:21:25 -0700 Subject: [PATCH 2/3] simpler Signed-off-by: Robert Kruszewski --- vortex-array/src/patches.rs | 95 +++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/vortex-array/src/patches.rs b/vortex-array/src/patches.rs index 73b65c754df..9a04806662f 100644 --- a/vortex-array/src/patches.rs +++ b/vortex-array/src/patches.rs @@ -1126,24 +1126,13 @@ fn filter_patches_with_mask( let mut kept_patches = Vec::with_capacity(expected_matches); if patch_count > true_count { - let mut gallop = PatchGallop { + gallop_patches_for_mask_indices( patch_indices, offset, - patch_position: 0, - mask_position: 0, - new_patch_indices: &mut new_patch_indices, - kept_patches: &mut kept_patches, - }; - - // `NoMoreMatches` only stops the search early, so the result is not an error. - let _ = match mask.cached_indices() { - Some(mask_indices) => mask_indices - .iter() - .try_for_each(|&mask_index| gallop.next_mask_index(mask_index)), - None => mask - .bit_buffer() - .try_for_each_set_index(|mask_index| gallop.next_mask_index(mask_index)), - }; + mask, + &mut new_patch_indices, + &mut kept_patches, + )?; } else { match mask.cached_indices() { Some(mask_indices) => gallop_mask_indices_for_patches( @@ -1207,44 +1196,56 @@ fn gallop_lower_bound(values: &[T], start: usize, needle: &T) -> usize { low + values[low..high].partition_point(|value| value < needle) } -/// No later mask index can match a patch. -struct NoMoreMatches; - -/// Finds the patches for a sorted sequence of mask indices, for masks that are sparser than -/// the patches. +/// Finds the patch of each mask index, for masks that are sparser than the patches. /// -/// The search compares values in the patch index type, so the patches do not need conversion. -struct PatchGallop<'a, T> { - patch_indices: &'a [T], +/// Only the mask indices up to the last patch can match, so we do not visit the mask indices +/// after it. The search compares values in the patch index type, so the patches do not need +/// conversion. +fn gallop_patches_for_mask_indices( + patch_indices: &[T], offset: usize, - patch_position: usize, - mask_position: usize, - new_patch_indices: &'a mut BufferMut, - kept_patches: &'a mut Vec, -} + mask: &MaskValues, + new_patch_indices: &mut BufferMut, + kept_patches: &mut Vec, +) -> VortexResult<()> { + let Some(&last_patch_index) = patch_indices.last() else { + return Ok(()); + }; + let last_mask_index = patch_index_to_usize(last_patch_index, offset)?; -impl PatchGallop<'_, T> { - fn next_mask_index(&mut self, mask_index: usize) -> Result<(), NoMoreMatches> { - // Mask indices are sorted, so no later mask index fits in `T` either. - let needle = mask_index - .checked_add(self.offset) - .and_then(::from) - .ok_or(NoMoreMatches)?; - - self.patch_position = gallop_lower_bound(self.patch_indices, self.patch_position, &needle); - if self.patch_position == self.patch_indices.len() { - return Err(NoMoreMatches); - } + let mut patch_position = 0; + let mut mask_position = 0; + + let mut visit = |mask_index: usize| { + // The needle is at most the last patch index, so it fits in `T`, and the search always + // stops at a patch. + let needle = ::from(mask_index + offset) + .vortex_expect("mask index is at most the last patch index"); - if self.patch_indices[self.patch_position] == needle { - self.new_patch_indices.push(self.mask_position as u64); - self.kept_patches.push(self.patch_position); - self.patch_position += 1; + patch_position = gallop_lower_bound(patch_indices, patch_position, &needle); + if patch_indices[patch_position] == needle { + new_patch_indices.push(mask_position as u64); + kept_patches.push(patch_position); + patch_position += 1; } - self.mask_position += 1; - Ok(()) + mask_position += 1; + }; + + match mask.cached_indices() { + Some(mask_indices) => { + let end = mask_indices.partition_point(|&mask_index| mask_index <= last_mask_index); + mask_indices[..end] + .iter() + .for_each(|&mask_index| visit(mask_index)); + } + None => mask + .bit_buffer() + .slice(..=last_mask_index) + .for_each_set_index(visit), } + + Ok(()) } /// Finds the mask position of each patch, for patches that are not sparser than the mask. From 92b80e3d91d7cb49e4a8b97fd222184d1c022877 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 30 Sep 2026 18:23:32 -0700 Subject: [PATCH 3/3] comments Signed-off-by: Robert Kruszewski --- vortex-array/src/patches.rs | 38 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/vortex-array/src/patches.rs b/vortex-array/src/patches.rs index 9a04806662f..8cef6f6add5 100644 --- a/vortex-array/src/patches.rs +++ b/vortex-array/src/patches.rs @@ -1102,13 +1102,12 @@ where /// is a new set of `Patches` with the indices relative to the provided `mask` rank, and the /// patch values. /// -/// We search for each element of the smaller side in the larger side, so the cost is -/// logarithmic in the size of the larger side. We never build the mask indices here: +/// We search the larger side for each element of the smaller side, without building the mask +/// indices: /// -/// * More patches than mask indices: gallop through the patches for each mask index. The mask -/// indices come from the mask cache, or from a walk over the set bits of the mask bitmap. -/// * Otherwise, with cached mask indices: gallop through the mask indices for each patch. -/// * Otherwise: look up each patch in the mask bitmap and count the set bits before it. +/// * More patches than mask indices: gallop through the patches. +/// * Cached mask indices: gallop through the mask indices. +/// * Otherwise: rank each patch in the mask bitmap. fn filter_patches_with_mask( patch_indices: &[T], offset: usize, @@ -1120,7 +1119,7 @@ fn filter_patches_with_mask( let patch_count = patch_indices.len(); let expected_matches = true_count.min(patch_count); - // The position of each kept patch in the mask rank, and in the original patches. + // The mask rank and the original position of each kept patch. let mut new_patch_indices = BufferMut::::with_capacity_in(expected_matches, allocator.clone()); let mut kept_patches = Vec::with_capacity(expected_matches); @@ -1160,9 +1159,8 @@ fn filter_patches_with_mask( let new_patch_values = patch_values.filter(Mask::from_indices(patch_values.len(), kept_patches))?; - // SAFETY: each kept patch adds one index and one value, so their lengths are equal. The - // indices are non-nullable `u64` positions in the mask rank. They are strictly increasing - // because the mask indices are, and each one is smaller than `true_count`. + // SAFETY: there is one index and one value per kept patch. The indices are strictly + // increasing non-nullable `u64` mask ranks below `true_count`. Ok(Some(unsafe { Patches::new_unchecked( true_count, @@ -1178,9 +1176,7 @@ fn filter_patches_with_mask( /// Returns the first position at or after `start` whose value is not less than `needle`. /// -/// The search doubles its step until it passes `needle`, then does a binary search in the last -/// step. The cost is logarithmic in the distance from `start` to the result, which makes a -/// sequence of searches with increasing needles cheap when the needles are sparse. +/// The cost is logarithmic in the distance from `start` to the result. fn gallop_lower_bound(values: &[T], start: usize, needle: &T) -> usize { let mut low = start; let mut high = start; @@ -1198,9 +1194,7 @@ fn gallop_lower_bound(values: &[T], start: usize, needle: &T) -> usize { /// Finds the patch of each mask index, for masks that are sparser than the patches. /// -/// Only the mask indices up to the last patch can match, so we do not visit the mask indices -/// after it. The search compares values in the patch index type, so the patches do not need -/// conversion. +/// Mask indices after the last patch cannot match, so we skip them. fn gallop_patches_for_mask_indices( patch_indices: &[T], offset: usize, @@ -1217,8 +1211,7 @@ fn gallop_patches_for_mask_indices( let mut mask_position = 0; let mut visit = |mask_index: usize| { - // The needle is at most the last patch index, so it fits in `T`, and the search always - // stops at a patch. + // At most the last patch index, so it fits in `T` and the search stays in bounds. let needle = ::from(mask_index + offset) .vortex_expect("mask index is at most the last patch index"); @@ -1276,11 +1269,9 @@ fn gallop_mask_indices_for_patches( Ok(()) } -/// Finds the mask position of each patch from the mask bitmap, for patches that are not -/// sparser than the mask. This does not create the mask indices. +/// Finds the mask position of each patch from the mask bitmap, without building the mask indices. /// -/// The mask position of a kept patch is the number of set bits before it. We count only the bits -/// between two adjacent patches, so the total count work is one pass over the mask words. +/// The mask position is the number of set bits before the patch, counted between adjacent patches. fn rank_patches_in_mask_bitmap( patch_indices: &[T], offset: usize, @@ -1806,8 +1797,7 @@ mod test { ); } - /// Covers each filter algorithm: gallop over the patches with cached mask indices or with the - /// mask bitmap, gallop over cached mask indices, and rank in the mask bitmap. + /// Covers each filter algorithm. #[rstest] #[case::sparse_mask((0..1000).step_by(2).collect(), (0..1000).step_by(97).collect())] #[case::sparse_patches((0..1000).step_by(97).collect(), (0..1000).step_by(2).collect())]