Skip to main content

vortex_array/arrays/primitive/compute/take/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
5mod avx2;
6
7use std::ptr;
8use std::sync::LazyLock;
9
10use itertools::Itertools as _;
11use vortex_buffer::Buffer;
12use vortex_buffer::BufferMut;
13use vortex_error::VortexResult;
14use vortex_error::vortex_bail;
15use vortex_error::vortex_ensure;
16use vortex_error::vortex_err;
17use vortex_mask::Mask;
18
19use crate::ArrayRef;
20use crate::Columnar;
21use crate::IntoArray;
22use crate::array::ArrayView;
23use crate::arrays::ConstantArray;
24use crate::arrays::PiecewiseSequence;
25use crate::arrays::Primitive;
26use crate::arrays::PrimitiveArray;
27use crate::arrays::dict::TakeExecute;
28use crate::arrays::piecewise_sequence::constant_unsigned_usize;
29use crate::arrays::piecewise_sequence::maybe_contiguous_slices;
30use crate::builtins::ArrayBuiltins;
31use crate::dtype::DType;
32use crate::dtype::IntegerPType;
33use crate::dtype::NativePType;
34use crate::dtype::UnsignedPType;
35use crate::executor::ExecutionCtx;
36use crate::match_each_integer_ptype;
37use crate::match_each_native_ptype;
38use crate::match_each_unsigned_integer_ptype;
39use crate::scalar::Scalar;
40use crate::validity::Validity;
41
42// Kernel selection happens on the first call to `take` and uses a combination of compile-time
43// and runtime feature detection to infer the best kernel for the platform.
44static PRIMITIVE_TAKE_KERNEL: LazyLock<&'static dyn TakeImpl> = LazyLock::new(|| {
45    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
46    {
47        if is_x86_feature_detected!("avx2") {
48            &avx2::TakeKernelAVX2
49        } else {
50            &TakeKernelScalar
51        }
52    }
53
54    #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
55    {
56        &TakeKernelScalar
57    }
58});
59
60trait TakeImpl: Send + Sync {
61    fn take(
62        &self,
63        array: ArrayView<'_, Primitive>,
64        indices: ArrayView<'_, Primitive>,
65        validity: Validity,
66    ) -> VortexResult<ArrayRef>;
67}
68
69struct TakeKernelScalar;
70
71impl TakeImpl for TakeKernelScalar {
72    fn take(
73        &self,
74        array: ArrayView<'_, Primitive>,
75        indices: ArrayView<'_, Primitive>,
76        validity: Validity,
77    ) -> VortexResult<ArrayRef> {
78        match_each_native_ptype!(array.ptype(), |T| {
79            match_each_integer_ptype!(indices.ptype(), |I| {
80                let values = take_primitive_scalar(array.as_slice::<T>(), indices.as_slice::<I>());
81                Ok(PrimitiveArray::new(values, validity).into_array())
82            })
83        })
84    }
85}
86
87impl TakeExecute for Primitive {
88    fn take(
89        array: ArrayView<'_, Primitive>,
90        indices: &ArrayRef,
91        ctx: &mut ExecutionCtx,
92    ) -> VortexResult<Option<ArrayRef>> {
93        if let Some(piecewise_indices) = indices.as_opt::<PiecewiseSequence>()
94            && let Some(taken) = take_contiguous_ranges(array, piecewise_indices, indices, ctx)?
95        {
96            return Ok(Some(taken));
97        }
98
99        let DType::Primitive(ptype, null) = indices.dtype() else {
100            vortex_bail!("Invalid indices dtype: {}", indices.dtype())
101        };
102
103        let indices_validity = indices.validity()?;
104        // Null index lanes are semantically ignored, but their physical values may be out of
105        // bounds. Redirect those lanes to zero for the cast/gather, then restore the original index
106        // validity below.
107        let indices_nulls_zeroed = match indices_validity.execute_mask(indices.len(), ctx)? {
108            Mask::AllTrue(_) => indices.clone(),
109            Mask::AllFalse(_) => {
110                return Ok(Some(
111                    ConstantArray::new(Scalar::null(array.dtype().as_nullable()), indices.len())
112                        .into_array(),
113                ));
114            }
115            Mask::Values(_) => indices
116                .clone()
117                .fill_null(Scalar::from(0).cast(indices.dtype())?)?,
118        };
119
120        let unsigned_indices = if ptype.is_unsigned_int() {
121            indices_nulls_zeroed.execute::<PrimitiveArray>(ctx)?
122        } else {
123            // This will fail if all values cannot be converted to unsigned
124            indices_nulls_zeroed
125                .cast(DType::Primitive(ptype.to_unsigned(), *null))?
126                .execute::<PrimitiveArray>(ctx)?
127        };
128
129        let validity = array
130            .validity()?
131            .take(&unsigned_indices.clone().into_array())?
132            .and(indices_validity)?;
133        // Delegate to the best kernel based on the target CPU
134        {
135            let unsigned_indices = unsigned_indices.as_view();
136            PRIMITIVE_TAKE_KERNEL
137                .take(array, unsigned_indices, validity)
138                .map(Some)
139        }
140    }
141}
142
143fn take_contiguous_ranges(
144    array: ArrayView<'_, Primitive>,
145    indices: ArrayView<'_, PiecewiseSequence>,
146    indices_ref: &ArrayRef,
147    ctx: &mut ExecutionCtx,
148) -> VortexResult<Option<ArrayRef>> {
149    let Some((starts, lengths)) = maybe_contiguous_slices(indices, ctx)? else {
150        return Ok(None);
151    };
152    let validity = array.validity()?.take(indices_ref)?;
153    let output_len = indices_ref.len();
154    let taken = match lengths {
155        Columnar::Constant(lengths) => {
156            let length = constant_unsigned_usize(&lengths);
157            take_slices_constant_length(array, &starts, length, validity, output_len)?
158        }
159        Columnar::Canonical(lengths) => {
160            let lengths = lengths.into_primitive();
161            take_slices(array, &starts, &lengths, validity, output_len)?
162        }
163    };
164    Ok(Some(taken))
165}
166
167// Compiler may see this as unused based on enabled features
168#[inline(always)]
169fn take_primitive_scalar<T: Copy, I: IntegerPType>(buffer: &[T], indices: &[I]) -> Buffer<T> {
170    // NB: The simpler `indices.iter().map(|idx| buffer[idx.as_()]).collect()` generates suboptimal
171    // assembly where the buffer length is repeatedly loaded from the stack on each iteration.
172
173    let mut result = BufferMut::with_capacity(indices.len());
174    let ptr = result.spare_capacity_mut().as_mut_ptr().cast::<T>();
175
176    // This explicit loop with pointer writes keeps the length in a register and avoids per-element
177    // capacity checks from `push()`.
178    for (i, idx) in indices.iter().enumerate() {
179        // SAFETY: We reserved `indices.len()` capacity, so `ptr.add(i)` is valid.
180        unsafe { ptr.add(i).write(buffer[idx.as_()]) };
181    }
182
183    // SAFETY: We just wrote exactly `indices.len()` elements.
184    unsafe { result.set_len(indices.len()) };
185    result.freeze()
186}
187
188fn take_slices(
189    array: ArrayView<'_, Primitive>,
190    starts: &PrimitiveArray,
191    lengths: &PrimitiveArray,
192    validity: Validity,
193    output_len: usize,
194) -> VortexResult<ArrayRef> {
195    match_each_native_ptype!(array.ptype(), |T| {
196        take_slices_typed::<T>(array, starts, lengths, validity, output_len)
197    })
198}
199
200fn take_slices_typed<T>(
201    array: ArrayView<'_, Primitive>,
202    starts: &PrimitiveArray,
203    lengths: &PrimitiveArray,
204    validity: Validity,
205    output_len: usize,
206) -> VortexResult<ArrayRef>
207where
208    T: NativePType,
209{
210    match_each_unsigned_integer_ptype!(starts.ptype(), |S| {
211        take_slices_start_typed::<T, S>(array, starts, lengths, validity, output_len)
212    })
213}
214
215fn take_slices_start_typed<T, S>(
216    array: ArrayView<'_, Primitive>,
217    starts: &PrimitiveArray,
218    lengths: &PrimitiveArray,
219    validity: Validity,
220    output_len: usize,
221) -> VortexResult<ArrayRef>
222where
223    T: NativePType,
224    S: UnsignedPType,
225{
226    match_each_unsigned_integer_ptype!(lengths.ptype(), |L| {
227        let values = take_slices_to_buffer::<T, S, L>(
228            array.as_slice::<T>(),
229            starts.as_slice::<S>(),
230            lengths.as_slice::<L>(),
231            output_len,
232        )?;
233        Ok(PrimitiveArray::new(values, validity).into_array())
234    })
235}
236
237fn take_slices_to_buffer<T, S, L>(
238    source: &[T],
239    starts: &[S],
240    lengths: &[L],
241    output_len: usize,
242) -> VortexResult<Buffer<T>>
243where
244    T: Copy,
245    S: UnsignedPType,
246    L: UnsignedPType,
247{
248    let mut values = BufferMut::<T>::with_capacity(output_len);
249    let spare = &mut values.spare_capacity_mut()[..output_len];
250    let mut cursor = 0usize;
251    for (&start, &length) in starts.iter().zip_eq(lengths) {
252        let start = start.as_();
253        let length = length.as_();
254        let src = &source[start..][..length];
255        // SAFETY: `src` and the checked `spare` range have equal lengths and cannot overlap.
256        unsafe {
257            ptr::copy_nonoverlapping(
258                src.as_ptr(),
259                spare[cursor..][..src.len()].as_mut_ptr().cast::<T>(),
260                src.len(),
261            );
262        }
263        cursor += src.len();
264    }
265    // SAFETY: the loop initialized the prefix `0..cursor` of the spare capacity.
266    unsafe { values.set_len(cursor) };
267    vortex_ensure!(
268        values.len() == output_len,
269        "PiecewiseSequenceArray expanded length {} does not match declared length {output_len}",
270        values.len()
271    );
272    Ok(values.freeze())
273}
274
275fn take_slices_constant_length(
276    array: ArrayView<'_, Primitive>,
277    starts: &PrimitiveArray,
278    length: usize,
279    validity: Validity,
280    output_len: usize,
281) -> VortexResult<ArrayRef> {
282    match_each_native_ptype!(array.ptype(), |T| {
283        take_slices_constant_length_typed::<T>(array, starts, length, validity, output_len)
284    })
285}
286
287fn take_slices_constant_length_typed<T>(
288    array: ArrayView<'_, Primitive>,
289    starts: &PrimitiveArray,
290    length: usize,
291    validity: Validity,
292    output_len: usize,
293) -> VortexResult<ArrayRef>
294where
295    T: NativePType,
296{
297    match_each_unsigned_integer_ptype!(starts.ptype(), |S| {
298        let values = take_slices_constant_length_to_buffer::<T, S>(
299            array.as_slice::<T>(),
300            starts.as_slice::<S>(),
301            length,
302            output_len,
303        )?;
304        Ok(PrimitiveArray::new(values, validity).into_array())
305    })
306}
307
308fn take_slices_constant_length_to_buffer<T, S>(
309    source: &[T],
310    starts: &[S],
311    length: usize,
312    output_len: usize,
313) -> VortexResult<Buffer<T>>
314where
315    T: Copy,
316    S: UnsignedPType,
317{
318    let computed_len = starts
319        .len()
320        .checked_mul(length)
321        .ok_or_else(|| vortex_err!("PiecewiseSequenceArray output length overflows usize"))?;
322    vortex_ensure!(
323        computed_len == output_len,
324        "PiecewiseSequenceArray expanded length {computed_len} does not match declared length {output_len}"
325    );
326
327    let mut values = BufferMut::<T>::with_capacity(output_len);
328    let spare = &mut values.spare_capacity_mut()[..output_len];
329    let mut cursor = 0usize;
330    for &start in starts {
331        let start = start.as_();
332        let src = &source[start..][..length];
333        // SAFETY: `src` and the checked `spare` range have equal lengths and cannot overlap.
334        unsafe {
335            ptr::copy_nonoverlapping(
336                src.as_ptr(),
337                spare[cursor..][..src.len()].as_mut_ptr().cast::<T>(),
338                src.len(),
339            );
340        }
341        cursor += src.len();
342    }
343    // SAFETY: the loop initialized the prefix `0..cursor` of the spare capacity.
344    unsafe { values.set_len(cursor) };
345    vortex_ensure!(
346        values.len() == output_len,
347        "PiecewiseSequenceArray expanded length {} does not match declared length {output_len}",
348        values.len()
349    );
350    Ok(values.freeze())
351}
352
353#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
354#[cfg(test)]
355mod test {
356    use rstest::rstest;
357    use vortex_buffer::buffer;
358    use vortex_error::VortexExpect;
359
360    use crate::IntoArray;
361    use crate::VortexSessionExecute;
362    use crate::array_session;
363    use crate::arrays::BoolArray;
364    use crate::arrays::PrimitiveArray;
365    use crate::arrays::primitive::compute::take::take_primitive_scalar;
366    use crate::compute::conformance::take::test_take_conformance;
367    use crate::scalar::Scalar;
368    use crate::validity::Validity;
369
370    #[test]
371    fn test_take() {
372        let a = vec![1i32, 2, 3, 4, 5];
373        let result = take_primitive_scalar(&a, &[0, 0, 4, 2]);
374        assert_eq!(result.as_slice(), &[1i32, 1, 5, 3]);
375    }
376
377    #[test]
378    fn test_take_with_null_indices() {
379        let mut ctx = array_session().create_execution_ctx();
380        let values = PrimitiveArray::new(
381            buffer![1i32, 2, 3, 4, 5],
382            Validity::Array(BoolArray::from_iter([true, true, false, false, true]).into_array()),
383        );
384        let indices = PrimitiveArray::new(
385            buffer![0, 3, 4],
386            Validity::Array(BoolArray::from_iter([true, true, false]).into_array()),
387        );
388        let actual = values.take(indices.into_array()).unwrap();
389        assert_eq!(
390            actual.execute_scalar(0, &mut ctx).vortex_expect("no fail"),
391            Scalar::from(Some(1))
392        );
393        // position 3 is null
394        assert_eq!(
395            actual.execute_scalar(1, &mut ctx).vortex_expect("no fail"),
396            Scalar::null_native::<i32>()
397        );
398        // the third index is null
399        assert_eq!(
400            actual.execute_scalar(2, &mut ctx).vortex_expect("no fail"),
401            Scalar::null_native::<i32>()
402        );
403    }
404
405    #[rstest]
406    #[case(PrimitiveArray::new(buffer![42i32], Validity::NonNullable))]
407    #[case(PrimitiveArray::new(buffer![0, 1], Validity::NonNullable))]
408    #[case(PrimitiveArray::new(buffer![0, 1, 2, 3, 4], Validity::NonNullable))]
409    #[case(PrimitiveArray::new(buffer![0, 1, 2, 3, 4, 5, 6, 7], Validity::NonNullable))]
410    #[case(PrimitiveArray::new(buffer![0, 1, 2, 3, 4], Validity::AllValid))]
411    #[case(PrimitiveArray::new(
412        buffer![0, 1, 2, 3, 4, 5],
413        Validity::Array(BoolArray::from_iter([true, false, true, false, true, true]).into_array()),
414    ))]
415    #[case(PrimitiveArray::from_option_iter([Some(1), None, Some(3), Some(4), None]))]
416    fn test_take_primitive_conformance(#[case] array: PrimitiveArray) {
417        test_take_conformance(
418            &array.into_array(),
419            &mut array_session().create_execution_ctx(),
420        );
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use vortex_buffer::buffer;
427
428    use crate::IntoArray;
429    use crate::VortexSessionExecute;
430    use crate::array_session;
431    use crate::arrays::BoolArray;
432    use crate::arrays::PrimitiveArray;
433    use crate::assert_arrays_eq;
434    use crate::validity::Validity;
435
436    #[test]
437    fn take_null_index_skips_out_of_bounds_value() {
438        let mut ctx = array_session().create_execution_ctx();
439        let values = PrimitiveArray::from_iter([10i32, 20, 30]);
440        let indices = PrimitiveArray::new(
441            buffer![1u64, 3],
442            Validity::Array(BoolArray::from_iter([true, false]).into_array()),
443        );
444
445        let taken = values.take(indices.into_array()).unwrap();
446
447        assert_arrays_eq!(
448            taken,
449            PrimitiveArray::from_option_iter([Some(20i32), None]).into_array(),
450            &mut ctx
451        );
452    }
453}