Skip to content

Commit 802b890

Browse files
authored
Add BooleanBufferBuilder::extend_trusted_len (#9137)
# Which issue does this PR close? - Part of #9136 # Rationale for this change API for #8951 , as part of a 2-3x speedup for filtering primitive types. # What changes are included in this PR? Adds `BooleanBufferBuilder::extend`, a fast way to extend the buffer from an iterator. # Are these changes tested? Yes, new tests # Are there any user-facing changes?
1 parent 601be25 commit 802b890

File tree

3 files changed

+226
-0
lines changed

3 files changed

+226
-0
lines changed

arrow-buffer/src/buffer/boolean.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,4 +800,27 @@ mod tests {
800800
}
801801
}
802802
}
803+
804+
#[test]
805+
fn test_extend_trusted_len_sets_byte_len() {
806+
// Ensures extend_trusted_len keeps the underlying byte length in sync with bit length.
807+
let mut builder = BooleanBufferBuilder::new(0);
808+
let bools: Vec<_> = (0..10).map(|i| i % 2 == 0).collect();
809+
unsafe { builder.extend_trusted_len(bools.into_iter()) };
810+
assert_eq!(builder.as_slice().len(), bit_util::ceil(builder.len(), 8));
811+
}
812+
813+
#[test]
814+
fn test_extend_trusted_len_then_append() {
815+
// Exercises append after extend_trusted_len to validate byte length and values.
816+
let mut builder = BooleanBufferBuilder::new(0);
817+
let bools: Vec<_> = (0..9).map(|i| i % 3 == 0).collect();
818+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
819+
builder.append(true);
820+
assert_eq!(builder.as_slice().len(), bit_util::ceil(builder.len(), 8));
821+
let finished = builder.finish();
822+
for (i, v) in bools.into_iter().chain(std::iter::once(true)).enumerate() {
823+
assert_eq!(finished.value(i), v, "at index {}", i);
824+
}
825+
}
803826
}

arrow-buffer/src/buffer/mutable.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,134 @@ impl MutableBuffer {
623623
buffer
624624
}
625625

626+
/// Extends this buffer with boolean values.
627+
///
628+
/// This requires `iter` to report an exact size via `size_hint`.
629+
/// `offset` indicates the starting offset in bits in this buffer to begin writing to
630+
/// and must be less than or equal to the current length of this buffer.
631+
/// All bits not written to (but readable due to byte alignment) will be zeroed out.
632+
/// # Safety
633+
/// Callers must ensure that `iter` reports an exact size via `size_hint`.
634+
#[inline]
635+
pub unsafe fn extend_bool_trusted_len<I: Iterator<Item = bool>>(
636+
&mut self,
637+
mut iter: I,
638+
offset: usize,
639+
) {
640+
let (lower, upper) = iter.size_hint();
641+
let len = upper.expect("Iterator must have exact size_hint");
642+
assert_eq!(lower, len, "Iterator must have exact size_hint");
643+
debug_assert!(
644+
offset <= self.len * 8,
645+
"offset must be <= buffer length in bits"
646+
);
647+
648+
if len == 0 {
649+
return;
650+
}
651+
652+
let start_len = offset;
653+
let end_bit = start_len + len;
654+
655+
// SAFETY: we will initialize all newly exposed bytes before they are read
656+
let new_len_bytes = bit_util::ceil(end_bit, 8);
657+
if new_len_bytes > self.len {
658+
self.reserve(new_len_bytes - self.len);
659+
// SAFETY: caller will initialize all newly exposed bytes before they are read
660+
unsafe { self.set_len(new_len_bytes) };
661+
}
662+
663+
let slice = self.as_slice_mut();
664+
665+
let mut bit_idx = start_len;
666+
667+
// ---- Unaligned prefix: advance to the next 64-bit boundary ----
668+
let misalignment = bit_idx & 63;
669+
let prefix_bits = if misalignment == 0 {
670+
0
671+
} else {
672+
(64 - misalignment).min(end_bit - bit_idx)
673+
};
674+
675+
if prefix_bits != 0 {
676+
let byte_start = bit_idx / 8;
677+
let byte_end = bit_util::ceil(bit_idx + prefix_bits, 8);
678+
let bit_offset = bit_idx % 8;
679+
680+
// Clear any newly-visible bits in the existing partial byte
681+
if bit_offset != 0 {
682+
let keep_mask = (1u8 << bit_offset).wrapping_sub(1);
683+
slice[byte_start] &= keep_mask;
684+
}
685+
686+
// Zero any new bytes we will partially fill in this prefix
687+
let zero_from = if bit_offset == 0 {
688+
byte_start
689+
} else {
690+
byte_start + 1
691+
};
692+
if byte_end > zero_from {
693+
slice[zero_from..byte_end].fill(0);
694+
}
695+
696+
for _ in 0..prefix_bits {
697+
let v = iter.next().unwrap();
698+
if v {
699+
let byte_idx = bit_idx / 8;
700+
let bit = bit_idx % 8;
701+
slice[byte_idx] |= 1 << bit;
702+
}
703+
bit_idx += 1;
704+
}
705+
}
706+
707+
if bit_idx < end_bit {
708+
// ---- Aligned middle: write u64 chunks ----
709+
debug_assert_eq!(bit_idx & 63, 0);
710+
let remaining_bits = end_bit - bit_idx;
711+
let chunks = remaining_bits / 64;
712+
713+
let words_start = bit_idx / 8;
714+
let words_end = words_start + chunks * 8;
715+
for dst in slice[words_start..words_end].chunks_exact_mut(8) {
716+
let mut packed: u64 = 0;
717+
for i in 0..64 {
718+
packed |= (iter.next().unwrap() as u64) << i;
719+
}
720+
dst.copy_from_slice(&packed.to_le_bytes());
721+
bit_idx += 64;
722+
}
723+
724+
// ---- Unaligned suffix: remaining < 64 bits ----
725+
let suffix_bits = end_bit - bit_idx;
726+
if suffix_bits != 0 {
727+
debug_assert_eq!(bit_idx % 8, 0);
728+
let byte_start = bit_idx / 8;
729+
let byte_end = bit_util::ceil(end_bit, 8);
730+
slice[byte_start..byte_end].fill(0);
731+
732+
for _ in 0..suffix_bits {
733+
let v = iter.next().unwrap();
734+
if v {
735+
let byte_idx = bit_idx / 8;
736+
let bit = bit_idx % 8;
737+
slice[byte_idx] |= 1 << bit;
738+
}
739+
bit_idx += 1;
740+
}
741+
}
742+
}
743+
744+
// Clear any unused bits in the last byte
745+
let remainder = end_bit % 8;
746+
if remainder != 0 {
747+
let mask = (1u8 << remainder).wrapping_sub(1);
748+
slice[bit_util::ceil(end_bit, 8) - 1] &= mask;
749+
}
750+
751+
debug_assert_eq!(bit_idx, end_bit);
752+
}
753+
626754
/// Register this [`MutableBuffer`] with the provided [`MemoryPool`]
627755
///
628756
/// This claims the memory used by this buffer in the pool, allowing for

arrow-buffer/src/builder/boolean.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,20 @@ impl BooleanBufferBuilder {
259259
pub fn finish_cloned(&self) -> BooleanBuffer {
260260
BooleanBuffer::new(Buffer::from_slice_ref(self.as_slice()), 0, self.len)
261261
}
262+
263+
/// Extends the builder from a trusted length iterator of booleans.
264+
/// # Safety
265+
/// Callers must ensure that `iter` reports an exact size via `size_hint`.
266+
///
267+
#[inline]
268+
pub unsafe fn extend_trusted_len<I>(&mut self, iterator: I)
269+
where
270+
I: Iterator<Item = bool>,
271+
{
272+
let len = iterator.size_hint().0;
273+
unsafe { self.buffer.extend_bool_trusted_len(iterator, self.len) };
274+
self.len += len;
275+
}
262276
}
263277

264278
impl From<BooleanBufferBuilder> for Buffer {
@@ -526,4 +540,65 @@ mod tests {
526540
assert_eq!(buf.len(), buf2.inner().len());
527541
assert_eq!(buf.as_slice(), buf2.values());
528542
}
543+
544+
#[test]
545+
fn test_extend() {
546+
let mut builder = BooleanBufferBuilder::new(0);
547+
let bools = vec![true, false, true, true, false, true, true, true, false];
548+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
549+
assert_eq!(builder.len(), 9);
550+
let finished = builder.finish();
551+
for (i, v) in bools.into_iter().enumerate() {
552+
assert_eq!(finished.value(i), v);
553+
}
554+
555+
// Test > 64 bits
556+
let mut builder = BooleanBufferBuilder::new(0);
557+
let bools: Vec<_> = (0..100).map(|i| i % 3 == 0 || i % 7 == 0).collect();
558+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
559+
assert_eq!(builder.len(), 100);
560+
let finished = builder.finish();
561+
for (i, v) in bools.into_iter().enumerate() {
562+
assert_eq!(finished.value(i), v, "at index {}", i);
563+
}
564+
}
565+
566+
#[test]
567+
fn test_extend_misaligned() {
568+
// Test misaligned start
569+
for offset in 1..65 {
570+
let mut builder = BooleanBufferBuilder::new(0);
571+
builder.append_n(offset, false);
572+
573+
let bools: Vec<_> = (0..100).map(|i| i % 3 == 0 || i % 7 == 0).collect();
574+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
575+
assert_eq!(builder.len(), offset + 100);
576+
577+
let finished = builder.finish();
578+
for i in 0..offset {
579+
assert!(!finished.value(i));
580+
}
581+
for (i, v) in bools.into_iter().enumerate() {
582+
assert_eq!(finished.value(offset + i), v, "at index {}", offset + i);
583+
}
584+
}
585+
}
586+
587+
#[test]
588+
fn test_extend_misaligned_end() {
589+
for len in 1..130 {
590+
let mut builder = BooleanBufferBuilder::new(0);
591+
let mut bools: Vec<_> = (0..len).map(|i| i % 2 == 0).collect();
592+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
593+
unsafe { builder.extend_trusted_len(bools.clone().into_iter()) };
594+
let copy = bools.clone();
595+
bools.extend(copy);
596+
assert_eq!(builder.len(), 2 * len);
597+
598+
let finished = builder.finish();
599+
for (i, &v) in bools.iter().enumerate() {
600+
assert_eq!(finished.value(i), v, "at index {} for len {}", i, len);
601+
}
602+
}
603+
}
529604
}

0 commit comments

Comments
 (0)