This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new c6f031305b perf: optimize take bool & take null buffers (#10813)
c6f031305b is described below
commit c6f031305be81f1ea3f41e88805bd87bb5848d5e
Author: RIchard Baah <[email protected]>
AuthorDate: Tue Sep 1 22:20:24 2026 -0400
perf: optimize take bool & take null buffers (#10813)
# Which issue does this PR close?
<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->
- works towards #8879.
# Rationale for this change
take on boolean arrays was slower than necessary: the nullable path
called take_bits twice (once for values, once for validity), and the
non-nullable path used high-level accessors that prevented the compiler
from issuing parallel loads.
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->
# What changes are included in this PR?
- `take_bits`: rewrites the None-nulls path to build output one byte at
a time with an 8-element inner loop, letting the compiler fully unroll
it and issue 8 source loads in parallel. Branches on the CHECKED const
generic to use value_unchecked when bounds are already guaranteed by the
caller.
- `take_bits_with_validity`: new single-pass helper that gathers value
bits and validity bits together when the source boolean array itself has
nulls, avoiding the previous double traversal.
- `take_boolean`: updated to use take_bits_with_validity in the nullable
case.
- ~~`take_record_batch_unchecked`: new pub unsafe counterpart to
`take_record_batch` that calls `take_impl::<_, false>` directly,
allowing us to use unsafe accessor methods~~
<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->
# Are these changes tested?
yes, I added new several boolean test
<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code
If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
If this PR claims a performance improvement, please include evidence
such as benchmark results.
-->
# Are there any user-facing changes?
~~yes new public method. `take_record_batch_unchecked()`~~
none
<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
If there are any breaking changes to public APIs, please call them out.
-->
---
arrow-select/src/take.rs | 349 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 328 insertions(+), 21 deletions(-)
diff --git a/arrow-select/src/take.rs b/arrow-select/src/take.rs
index 55b3d521ab..14906206e6 100644
--- a/arrow-select/src/take.rs
+++ b/arrow-select/src/take.rs
@@ -94,10 +94,10 @@ pub fn take(
let options = options.unwrap_or_default();
downcast_integer_array!(
indices => {
+ let indices = indices.to_indices();
if options.check_bounds {
- check_bounds(values.len(), indices)?;
+ check_bounds(values.len(), &indices)?;
}
- let indices = indices.to_indices();
take_impl::<_, true>(values, &indices)
},
d => Err(ArrowError::InvalidArgumentError(format!("Take only supported
for integers, got {d:?}")))
@@ -509,43 +509,252 @@ fn take_native<T: ArrowNativeType, I:
ArrowPrimitiveType>(
}
}
+/// Read the bit at `src_bit_idx` from `src` and, if it is set, write a `1` to
`dst_bit_idx`
+/// in `dst`. Leaves `dst_bit_idx` unchanged (zero) when the source bit is
unset.
+///
+/// ```text
+/// src = 0b00100000 (bit 5 is set)
+/// copy_bit_if_set(src, 5, dst, 2) → dst bit 2 becomes 1
+/// ```
+///
+/// # Safety
+/// - `src` must be valid for reads up to byte `src_bit_idx / 8`.
+/// - `dst` must be valid for writes up to byte `dst_bit_idx / 8`.
+#[inline(always)]
+unsafe fn copy_bit_if_set(src: *const u8, src_bit_idx: usize, dst: *mut u8,
dst_bit_idx: usize) {
+ unsafe {
+ if bit_util::get_bit_raw(src, src_bit_idx) {
+ bit_util::set_bit_raw(dst, dst_bit_idx);
+ }
+ }
+}
+
+/// Read the bit at `bit_idx` from `src` and return it shifted to `out_pos`,
ready to be
+/// OR'd into an output byte accumulator.
+///
+/// ```text
+/// src = 0b10100000 (bit 5 is set)
+/// pack_bit(src, 5, 2) → 0b00000100 (bit from position 5, placed at
position 2)
+/// ```
+///
+/// # Safety
+/// `src` must be valid for reads up to byte `bit_idx / 8`.
+#[inline(always)]
+unsafe fn pack_bit(src: *const u8, bit_idx: usize, out_pos: usize) -> u8 {
+ let byte = unsafe { *src.add(bit_idx >> 3) }; // byte containing bit
`bit_idx`
+ ((byte >> (bit_idx & 7)) & 1) << out_pos // extract the bit, shift to
output position
+}
+
#[inline(never)]
fn take_bits<I: ArrowPrimitiveType, const CHECKED: bool>(
values: &BooleanBuffer,
indices: &PrimitiveArray<I>,
) -> BooleanBuffer {
let len = indices.len();
-
- match indices.nulls().filter(|n| n.null_count() > 0) {
- Some(nulls) => {
- let mut output_buffer = MutableBuffer::new_null(len);
- let output_slice = output_buffer.as_slice_mut();
- nulls.valid_indices().for_each(|idx| {
- // SAFETY: idx is a valid index in indices.nulls() -->
idx<indices.len()
- if unsafe {
values.value(indices.value_unchecked(idx).as_usize()) } {
- // SAFETY: MutableBuffer was created with space for
indices.len() bit, and idx < indices.len()
- unsafe { bit_util::set_bit_raw(output_slice.as_mut_ptr(),
idx) };
+ let src_offset = values.offset();
+ let src_ptr = values.values().as_ptr();
+ let out_bytes = len.div_ceil(8);
+
+ match indices.nulls().filter(|nulls| nulls.null_count() > 0) {
+ Some(index_nulls) => {
+ let mut output = vec![0u8; out_bytes];
+ let out_ptr = output.as_mut_ptr();
+ index_nulls.valid_indices().for_each(|valid_idx| {
+ // SAFETY: valid_idx < indices.len(), guaranteed by
valid_indices().
+ let index_val = unsafe { indices.value_unchecked(valid_idx)
}.as_usize();
+ if CHECKED {
+ if values.value(index_val) {
+ // SAFETY: valid_idx < indices.len() = len, output
buffer holds len bits.
+ unsafe { bit_util::set_bit_raw(out_ptr, valid_idx) };
+ }
+ } else {
+ // SAFETY: caller guarantees index_val < values.len().
+ unsafe { copy_bit_if_set(src_ptr, index_val + src_offset,
out_ptr, valid_idx) };
}
});
- BooleanBuffer::new(output_buffer.into(), 0, len)
+ BooleanBuffer::new(Buffer::from(output), 0, len)
}
None => {
- BooleanBuffer::collect_bool(len, |idx: usize| {
- // SAFETY: idx<indices.len()
- values.value(unsafe { indices.value_unchecked(idx).as_usize()
})
- })
+ // Build the output byte-by-byte with an 8-element inner loop so
the
+ // compiler can fully unroll it and issue the 8 source loads in
parallel.
+ let mut output = vec![0u8; out_bytes];
+ let out_slice = output.as_mut_slice();
+ let full_bytes = len / 8;
+
+ for (byte_idx, out_byte) in
out_slice.iter_mut().enumerate().take(full_bytes) {
+ let base = byte_idx * 8;
+ let mut byte = 0u8;
+ for bit in 0..8usize {
+ // SAFETY: base + bit < full_bytes * 8 <= len, so base +
bit is a valid
+ // position in the indices array.
+ let index_val = unsafe { indices.value_unchecked(base +
bit) }.as_usize();
+ if CHECKED {
+ byte |= (values.value(index_val) as u8) << bit;
+ } else {
+ // SAFETY: caller guarantees index_val < values.len().
+ byte |= unsafe { pack_bit(src_ptr, index_val +
src_offset, bit) };
+ }
+ }
+ *out_byte = byte;
+ }
+ // Handle remaining bits when len is not a multiple of 8.
+ if full_bytes < out_bytes {
+ let base = full_bytes * 8;
+ let mut byte = 0u8;
+ for bit in 0..(len - base) {
+ // SAFETY: base + bit < len (remainder loop bound), so
base + bit is a
+ // valid position in the indices array.
+ let index_val = unsafe { indices.value_unchecked(base +
bit) }.as_usize();
+ if CHECKED {
+ byte |= (values.value(index_val) as u8) << bit;
+ } else {
+ // SAFETY: caller guarantees index_val < values.len().
+ byte |= unsafe { pack_bit(src_ptr, index_val +
src_offset, bit) };
+ }
+ }
+ out_slice[full_bytes] = byte;
+ }
+ BooleanBuffer::new(Buffer::from(output), 0, len)
+ }
+ }
+}
+
+/// Gather value bits and validity bits from two boolean buffers in a single
pass.
+/// Used when the values array itself has nulls, avoiding two separate
`take_bits` calls.
+#[inline(never)]
+fn take_bits_with_validity<I: ArrowPrimitiveType, const CHECKED: bool>(
+ values: &BooleanBuffer,
+ validity: &BooleanBuffer,
+ indices: &PrimitiveArray<I>,
+) -> (BooleanBuffer, Option<NullBuffer>) {
+ let len = indices.len();
+ let value_bit_offset = values.offset();
+ let validity_bit_offset = validity.offset();
+ let value_data_ptr = values.values().as_ptr();
+ let validity_data_ptr = validity.values().as_ptr();
+ let out_bytes = len.div_ceil(8);
+
+ let mut value_out = vec![0u8; out_bytes];
+ let mut validity_out = vec![0u8; out_bytes];
+
+ match indices.nulls().filter(|nulls| nulls.null_count() > 0) {
+ Some(index_nulls) => {
+ let value_out_ptr = value_out.as_mut_ptr();
+ let validity_out_ptr = validity_out.as_mut_ptr();
+ for out_pos in index_nulls.valid_indices() {
+ // SAFETY: out_pos < indices.len(), guaranteed by
valid_indices().
+ let src_idx = unsafe { indices.value_unchecked(out_pos)
}.as_usize();
+ if CHECKED {
+ if values.value(src_idx) {
+ // SAFETY: out_pos < indices.len() = len, output
buffer holds len bits.
+ unsafe { bit_util::set_bit_raw(value_out_ptr, out_pos)
};
+ }
+ if validity.value(src_idx) {
+ // SAFETY: out_pos < indices.len() = len, output
buffer holds len bits.
+ unsafe { bit_util::set_bit_raw(validity_out_ptr,
out_pos) };
+ }
+ } else {
+ // SAFETY: caller guarantees src_idx < values.len().
+ unsafe {
+ copy_bit_if_set(
+ value_data_ptr,
+ src_idx + value_bit_offset,
+ value_out_ptr,
+ out_pos,
+ );
+ copy_bit_if_set(
+ validity_data_ptr,
+ src_idx + validity_bit_offset,
+ validity_out_ptr,
+ out_pos,
+ );
+ }
+ }
+ }
+ }
+ None => {
+ let value_out_slice = value_out.as_mut_slice();
+ let validity_out_slice = validity_out.as_mut_slice();
+ let full_bytes = len / 8;
+
+ for (byte_idx, (value_out_byte, validity_out_byte)) in
value_out_slice
+ .iter_mut()
+ .zip(validity_out_slice.iter_mut())
+ .enumerate()
+ .take(full_bytes)
+ {
+ let bit_base = byte_idx * 8;
+ let mut packed_values = 0u8;
+ let mut packed_validity = 0u8;
+ for bit_pos in 0..8usize {
+ // SAFETY: bit_base + bit_pos < full_bytes * 8 <= len.
+ let src_idx = unsafe { indices.value_unchecked(bit_base +
bit_pos) }.as_usize();
+ if CHECKED {
+ packed_values |= (values.value(src_idx) as u8) <<
bit_pos;
+ packed_validity |= (validity.value(src_idx) as u8) <<
bit_pos;
+ } else {
+ // SAFETY: caller guarantees src_idx < values.len().
+ packed_values |= unsafe {
+ pack_bit(value_data_ptr, src_idx +
value_bit_offset, bit_pos)
+ };
+ packed_validity |= unsafe {
+ pack_bit(validity_data_ptr, src_idx +
validity_bit_offset, bit_pos)
+ };
+ }
+ }
+ *value_out_byte = packed_values;
+ *validity_out_byte = packed_validity;
+ }
+ // Handle remaining bits when len is not a multiple of 8.
+ if full_bytes < out_bytes {
+ let bit_base = full_bytes * 8;
+ let mut packed_values = 0u8;
+ let mut packed_validity = 0u8;
+ for bit_pos in 0..(len - bit_base) {
+ // SAFETY: bit_base + bit_pos < len (remainder loop bound).
+ let src_idx = unsafe { indices.value_unchecked(bit_base +
bit_pos) }.as_usize();
+ if CHECKED {
+ packed_values |= (values.value(src_idx) as u8) <<
bit_pos;
+ packed_validity |= (validity.value(src_idx) as u8) <<
bit_pos;
+ } else {
+ // SAFETY: caller guarantees src_idx < values.len().
+ packed_values |= unsafe {
+ pack_bit(value_data_ptr, src_idx +
value_bit_offset, bit_pos)
+ };
+ packed_validity |= unsafe {
+ pack_bit(validity_data_ptr, src_idx +
validity_bit_offset, bit_pos)
+ };
+ }
+ }
+ value_out_slice[full_bytes] = packed_values;
+ validity_out_slice[full_bytes] = packed_validity;
+ }
}
}
+
+ let value_buf_out = BooleanBuffer::new(Buffer::from(value_out), 0, len);
+ let validity_buf_out = NullBuffer::from_unsliced_buffer(validity_out, len);
+ (value_buf_out, validity_buf_out)
}
/// `take` implementation for boolean arrays
fn take_boolean<IndexType: ArrowPrimitiveType, const CHECKED: bool>(
- values: &BooleanArray,
+ array: &BooleanArray,
indices: &PrimitiveArray<IndexType>,
) -> BooleanArray {
- let val_buf = take_bits::<_, CHECKED>(values.values(), indices);
- let null_buf = take_nulls::<_, CHECKED>(values.nulls(), indices);
- BooleanArray::new(val_buf, null_buf)
+ let bits = array.values();
+ match array.nulls().filter(|n| n.null_count() > 0) {
+ Some(array_nulls) => {
+ let (val_buf, null_buf) =
+ take_bits_with_validity::<_, CHECKED>(bits,
array_nulls.inner(), indices);
+ BooleanArray::new(val_buf, null_buf)
+ }
+ None => {
+ let val_buf = take_bits::<_, CHECKED>(bits, indices);
+ let null_buf = take_nulls::<_, CHECKED>(None, indices);
+ BooleanArray::new(val_buf, null_buf)
+ }
+ }
}
/// `take` implementation for string arrays
@@ -1946,6 +2155,104 @@ mod tests {
);
}
+ #[test]
+ // Null indices + sliced source boolean array — exercises src_offset in
the sparse take_bits path.
+ fn test_take_bool_nullable_index_sliced_source() {
+ let source = BooleanArray::from(vec![Some(true), Some(false),
Some(true), Some(false)]);
+ let source = source.slice(1, 3); // logical: [false, true, false],
offset=1
+ let source = source;
+
+ let indices = UInt32Array::from(vec![Some(2), None, Some(0)]);
+ let result = take(&source, &indices, None).unwrap();
+ let result = result.as_any().downcast_ref::<BooleanArray>().unwrap();
+
+ let expected = BooleanArray::from(vec![Some(false), None,
Some(false)]);
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ // >8 elements: exercises the 8-at-a-time unrolled byte packing in
take_bits.
+ fn test_take_bool_no_nulls_multi_byte() {
+ let source = BooleanArray::from(vec![
+ true, false, true, true, false, false, true, false, true, true,
+ ]);
+ let indices = UInt32Array::from(vec![0, 2, 4, 6, 8, 1, 3, 5, 7, 9]);
+ let result = take(&source, &indices, None).unwrap();
+ let result = result.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let expected = BooleanArray::from(vec![
+ true, true, false, true, true, false, true, false, false, true,
+ ]);
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ // Sliced source: verifies src_offset is applied when no null indices.
+ fn test_take_bool_no_nulls_sliced_source() {
+ let source = BooleanArray::from(vec![true, false, true, false, true]);
+ let source = source.slice(2, 3); // [true, false, true], offset=2
+ let source = source.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let indices = UInt32Array::from(vec![2, 0, 1]);
+ let result = take(source, &indices, None).unwrap();
+ let result = result.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let expected = BooleanArray::from(vec![true, true, false]);
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ // Nullable source, >8 elements: exercises the 8-at-a-time unrolled byte
packing in take_bits_with_validity.
+ fn test_take_bool_nullable_values_multi_byte() {
+ let source = BooleanArray::from(vec![
+ Some(true),
+ None,
+ Some(false),
+ Some(true),
+ None,
+ Some(false),
+ Some(true),
+ Some(false),
+ Some(true),
+ None,
+ ]);
+ let indices = UInt32Array::from(vec![0, 2, 4, 6, 8, 1, 3, 5, 7, 9]);
+ let result = take(&source, &indices, None).unwrap();
+ let result = result.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let expected = BooleanArray::from(vec![
+ Some(true),
+ Some(false),
+ None,
+ Some(true),
+ Some(true),
+ None,
+ Some(true),
+ Some(false),
+ Some(false),
+ None,
+ ]);
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ // Nullable source, null indices, sliced source: verifies src_offset with
both null paths.
+ fn test_take_bool_nullable_values_sliced_source_null_indices() {
+ let source =
+ BooleanArray::from(vec![Some(true), Some(false), None, Some(true),
Some(false)]);
+ let source = source.slice(1, 4); // [false, null, true, false],
offset=1
+ let source = source.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let indices = UInt32Array::from(vec![Some(3), None, Some(1), Some(0)]);
+ let result = take(source, &indices, None).unwrap();
+ let result = result.as_any().downcast_ref::<BooleanArray>().unwrap();
+ let expected = BooleanArray::from(vec![Some(false), None, None,
Some(false)]);
+ assert_eq!(result, &expected);
+ }
+
+ #[test]
+ #[should_panic(expected = "assertion failed: idx < self.bit_len")]
+ fn test_take_bool_oob_no_check_bounds_panics() {
+ let array = BooleanArray::from(vec![true, false, true]);
+ let indices = Int32Array::from(vec![0, 1, 10]);
+ take(&array, &indices, None).unwrap();
+ }
+
fn _test_take_string<'a, K>()
where
K: Array + PartialEq + From<Vec<Option<&'a str>>> + 'static,