From cef61b2abbfe506180369622e0a0cfd1bbbb02be Mon Sep 17 00:00:00 2001 From: Daniel King Date: Fri, 17 Jul 2026 15:07:13 -0400 Subject: [PATCH 1/5] Add PiecewiseSequence take microbenchmarks Signed-off-by: Daniel King --- vortex-array/Cargo.toml | 8 + .../piecewise_sequence_take_primitive.rs | 163 ++++++++++++++ .../benches/take_slices_to_buffer_matrix.rs | 200 ++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 vortex-array/benches/piecewise_sequence_take_primitive.rs create mode 100644 vortex-array/benches/take_slices_to_buffer_matrix.rs diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index a1aa5b047f8..70fba4acdcf 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -227,6 +227,14 @@ harness = false name = "take_primitive" harness = false +[[bench]] +name = "piecewise_sequence_take_primitive" +harness = false + +[[bench]] +name = "take_slices_to_buffer_matrix" +harness = false + [[bench]] name = "take_struct" harness = false diff --git a/vortex-array/benches/piecewise_sequence_take_primitive.rs b/vortex-array/benches/piecewise_sequence_take_primitive.rs new file mode 100644 index 00000000000..189cfd1da41 --- /dev/null +++ b/vortex-array/benches/piecewise_sequence_take_primitive.rs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Primitive take benchmarks for [`PiecewiseSequenceArray`] length representations. +//! +//! The `reify_constant_lengths_then_take` case simulates removing the constant-length +//! specialization: it starts with constant lengths, materializes them to a dense primitive array, +//! and then executes the primitive take through the non-constant-length path. + +#![allow(clippy::cast_possible_truncation)] +#![expect(clippy::unwrap_used)] + +use std::sync::LazyLock; + +use divan::Bencher; +use divan::counter::BytesCount; +use rand::SeedableRng; +use rand::prelude::*; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::array_session; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::PiecewiseSequenceArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_session::VortexSession; + +fn main() { + LazyLock::force(&SESSION); + divan::main(); +} + +static SESSION: LazyLock = LazyLock::new(array_session); + +const SOURCE_LEN: usize = 8 * 1024 * 1024; +const OUTPUT_LEN: usize = 1024 * 1024; +const RUN_LENGTHS: &[usize] = &[1, 4, 16, 64, 256, 1024]; + +#[divan::bench(args = RUN_LENGTHS)] +fn optimized_constant_lengths(bencher: Bencher, run_length: usize) { + let values = values(); + let indices = piecewise_indices( + run_length, + lengths_constant(run_length, run_count(run_length)), + ); + + bencher + .counter(BytesCount::of_many::(OUTPUT_LEN)) + .with_inputs(|| (&values, indices.clone(), SESSION.create_execution_ctx())) + .bench_refs(|(values, indices, ctx)| { + divan::black_box( + values + .clone() + .take(indices.clone()) + .unwrap() + .execute::(ctx) + .unwrap(), + ); + }); +} + +#[divan::bench(args = RUN_LENGTHS)] +fn prebuilt_dense_lengths(bencher: Bencher, run_length: usize) { + let values = values(); + let indices = piecewise_indices(run_length, lengths_dense(run_length, run_count(run_length))); + + bencher + .counter(BytesCount::of_many::(OUTPUT_LEN)) + .with_inputs(|| (&values, indices.clone(), SESSION.create_execution_ctx())) + .bench_refs(|(values, indices, ctx)| { + divan::black_box( + values + .clone() + .take(indices.clone()) + .unwrap() + .execute::(ctx) + .unwrap(), + ); + }); +} + +#[divan::bench(args = RUN_LENGTHS)] +fn reify_constant_lengths_then_take(bencher: Bencher, run_length: usize) { + let values = values(); + let run_count = run_count(run_length); + let starts = starts(run_count, run_length).into_array(); + let lengths = lengths_constant(run_length, run_count); + let multipliers = ConstantArray::new(1u64, run_count).into_array(); + + bencher + .counter(BytesCount::of_many::(OUTPUT_LEN)) + .with_inputs(|| { + ( + &values, + starts.clone(), + lengths.clone(), + multipliers.clone(), + SESSION.create_execution_ctx(), + ) + }) + .bench_refs(|(values, starts, lengths, multipliers, ctx)| { + let lengths = lengths + .clone() + .execute::(ctx) + .unwrap() + .into_array(); + + // SAFETY: starts, reified lengths, and unit multipliers have the same run count, are + // non-null unsigned integer arrays, and expand to OUTPUT_LEN by construction. + let indices = unsafe { + PiecewiseSequenceArray::new_unchecked( + starts.clone(), + lengths, + multipliers.clone(), + OUTPUT_LEN, + ) + } + .into_array(); + + divan::black_box( + values + .clone() + .take(indices) + .unwrap() + .execute::(ctx) + .unwrap(), + ); + }); +} + +fn values() -> ArrayRef { + PrimitiveArray::from_iter((0..SOURCE_LEN).map(|idx| idx as i64)).into_array() +} + +fn run_count(run_length: usize) -> usize { + assert_eq!(OUTPUT_LEN % run_length, 0); + OUTPUT_LEN / run_length +} + +fn piecewise_indices(run_length: usize, lengths: ArrayRef) -> ArrayRef { + let run_count = run_count(run_length); + let starts = starts(run_count, run_length).into_array(); + let multipliers = ConstantArray::new(1u64, run_count).into_array(); + + // SAFETY: starts, lengths, and unit multipliers have the same run count, are non-null unsigned + // integer arrays, and expand to OUTPUT_LEN by construction. + unsafe { PiecewiseSequenceArray::new_unchecked(starts, lengths, multipliers, OUTPUT_LEN) } + .into_array() +} + +fn lengths_constant(run_length: usize, run_count: usize) -> ArrayRef { + ConstantArray::new(run_length as u64, run_count).into_array() +} + +fn lengths_dense(run_length: usize, run_count: usize) -> ArrayRef { + PrimitiveArray::from_iter((0..run_count).map(|_| run_length as u64)).into_array() +} + +fn starts(run_count: usize, run_length: usize) -> PrimitiveArray { + let mut rng = StdRng::seed_from_u64(0x51ced); + let max_start = SOURCE_LEN - run_length; + PrimitiveArray::from_iter((0..run_count).map(|_| rng.random_range(0..=max_start) as u64)) +} diff --git a/vortex-array/benches/take_slices_to_buffer_matrix.rs b/vortex-array/benches/take_slices_to_buffer_matrix.rs new file mode 100644 index 00000000000..fbe8a023cfc --- /dev/null +++ b/vortex-array/benches/take_slices_to_buffer_matrix.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Microbenchmarks for primitive `take_slices_to_buffer` copy-loop variants. +//! +//! The matrix covers: +//! - append via `BufferMut::extend_from_slice` vs direct cursor copy into spare output capacity +//! - ordinary checked slicing vs a preverification pass followed by unchecked slicing + +#![allow(clippy::cast_possible_truncation)] +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use divan::counter::BytesCount; +use rand::SeedableRng; +use rand::prelude::*; +use rand_distr::Distribution; +use rand_distr::Normal; +use vortex_buffer::Buffer; +use vortex_buffer::BufferMut; + +fn main() { + divan::main(); +} + +const SOURCE_LENS: &[usize] = &[1_000, 10_000, 100_000]; +const SLICE_COUNT: usize = 50; + +type TakeSlicesFn = fn(&[u16], &[usize], &[usize], usize) -> Buffer; + +#[derive(Clone)] +struct Case { + values: Vec, + starts: Vec, + lengths: Vec, + output_len: usize, +} + +#[divan::bench(args = SOURCE_LENS)] +fn extend_safe(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_extend_safe); +} + +#[divan::bench(args = SOURCE_LENS)] +fn cursor_copy_safe(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_cursor_copy_safe); +} + +#[divan::bench(args = SOURCE_LENS)] +fn preverify_extend_unchecked(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_preverify_extend_unchecked); +} + +#[divan::bench(args = SOURCE_LENS)] +fn preverify_cursor_copy_unchecked(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_preverify_cursor_copy_unchecked); +} + +fn bench_case(bencher: Bencher, case: Case, f: TakeSlicesFn) { + bencher + .counter(BytesCount::of_many::(case.output_len)) + .with_inputs(|| (&case.values, &case.starts, &case.lengths, case.output_len)) + .bench_refs(|(values, starts, lengths, output_len)| { + divan::black_box(f(values, starts, lengths, *output_len)); + }); +} + +impl Case { + fn new(source_len: usize) -> Self { + let mut rng = StdRng::seed_from_u64(source_len as u64 ^ 0x5eed_51ce); + let values = (0..source_len).map(|_| rng.random::()).collect(); + let mean = source_len as f64 / 100.0; + let normal = Normal::new(mean, (mean / 4.0).max(1.0)).unwrap(); + + let mut ranges = (0..SLICE_COUNT) + .map(|_| { + let length = normal + .sample(&mut rng) + .round() + .clamp(1.0, source_len as f64) as usize; + let start = rng.random_range(0..=source_len - length); + (start, length) + }) + .collect::>(); + ranges.sort_unstable_by_key(|&(start, _)| start); + + let output_len = ranges.iter().map(|&(_, length)| length).sum(); + let (starts, lengths) = ranges.into_iter().unzip(); + Self { + values, + starts, + lengths, + output_len, + } + } +} + +fn take_extend_safe( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + let mut result = BufferMut::::with_capacity(output_len); + for (&start, &length) in starts.iter().zip(lengths) { + result.extend_from_slice(&values[start..start + length]); + } + result.freeze() +} + +fn take_cursor_copy_safe( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + let mut result = BufferMut::::with_capacity(output_len); + let mut cursor = 0usize; + for (&start, &length) in starts.iter().zip(lengths) { + copy_to_spare(&mut result, cursor, &values[start..start + length]); + cursor += length; + } + + // SAFETY: the loop writes exactly `output_len` values into spare capacity. + unsafe { result.set_len(output_len) }; + result.freeze() +} + +fn take_preverify_extend_unchecked( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + preverify(values.len(), starts, lengths); + + let mut result = BufferMut::::with_capacity(output_len); + for (&start, &length) in starts.iter().zip(lengths) { + // SAFETY: `preverify` checked every source range. + unsafe { + result.extend_from_slice(values.get_unchecked(start..start + length)); + } + } + result.freeze() +} + +fn take_preverify_cursor_copy_unchecked( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + preverify(values.len(), starts, lengths); + + let mut result = BufferMut::::with_capacity(output_len); + let mut cursor = 0usize; + for (&start, &length) in starts.iter().zip(lengths) { + // SAFETY: `preverify` checked every source range. + let source = unsafe { values.get_unchecked(start..start + length) }; + unsafe { copy_to_spare_unchecked(&mut result, cursor, source) }; + cursor += length; + } + + // SAFETY: the loop writes exactly `output_len` values into spare capacity. + unsafe { result.set_len(output_len) }; + result.freeze() +} + +fn preverify(source_len: usize, starts: &[usize], lengths: &[usize]) { + for (&start, &length) in starts.iter().zip(lengths) { + let end = start.checked_add(length).unwrap(); + assert!(end <= source_len); + } +} + +fn copy_to_spare(result: &mut BufferMut, cursor: usize, source: &[u16]) { + let dst = &mut result.spare_capacity_mut()[cursor..][..source.len()]; + // SAFETY: `dst` has exactly `source.len()` spare slots and does not overlap with source. + unsafe { copy_to_uninit(dst.as_mut_ptr().cast(), source) }; +} + +unsafe fn copy_to_spare_unchecked(result: &mut BufferMut, cursor: usize, source: &[u16]) { + // SAFETY: callers ensure `cursor..cursor + source.len()` is within spare capacity. + let dst = unsafe { + result + .spare_capacity_mut() + .get_unchecked_mut(cursor..cursor + source.len()) + }; + // SAFETY: `dst` has exactly `source.len()` spare slots and does not overlap with source. + unsafe { copy_to_uninit(dst.as_mut_ptr().cast(), source) }; +} + +unsafe fn copy_to_uninit(dst: *mut u16, source: &[u16]) { + // SAFETY: callers ensure `dst` points to `source.len()` writable uninitialized u16 slots. + unsafe { std::ptr::copy_nonoverlapping(source.as_ptr(), dst, source.len()) }; +} From da523ec0213cf638cdee7a4cf3514330b10768be Mon Sep 17 00:00:00 2001 From: Daniel King Date: Fri, 17 Jul 2026 15:21:31 -0400 Subject: [PATCH 2/5] Make take slices matrix cursor copy checks explicit Signed-off-by: Daniel King --- .../benches/take_slices_to_buffer_matrix.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vortex-array/benches/take_slices_to_buffer_matrix.rs b/vortex-array/benches/take_slices_to_buffer_matrix.rs index fbe8a023cfc..cc81b726684 100644 --- a/vortex-array/benches/take_slices_to_buffer_matrix.rs +++ b/vortex-array/benches/take_slices_to_buffer_matrix.rs @@ -121,9 +121,12 @@ fn take_cursor_copy_safe( let mut result = BufferMut::::with_capacity(output_len); let mut cursor = 0usize; for (&start, &length) in starts.iter().zip(lengths) { + let end = cursor.checked_add(length).unwrap(); + assert!(end <= output_len); copy_to_spare(&mut result, cursor, &values[start..start + length]); - cursor += length; + cursor = end; } + assert_eq!(cursor, output_len); // SAFETY: the loop writes exactly `output_len` values into spare capacity. unsafe { result.set_len(output_len) }; @@ -136,7 +139,7 @@ fn take_preverify_extend_unchecked( lengths: &[usize], output_len: usize, ) -> Buffer { - preverify(values.len(), starts, lengths); + preverify(values.len(), starts, lengths, output_len); let mut result = BufferMut::::with_capacity(output_len); for (&start, &length) in starts.iter().zip(lengths) { @@ -154,13 +157,14 @@ fn take_preverify_cursor_copy_unchecked( lengths: &[usize], output_len: usize, ) -> Buffer { - preverify(values.len(), starts, lengths); + preverify(values.len(), starts, lengths, output_len); let mut result = BufferMut::::with_capacity(output_len); let mut cursor = 0usize; for (&start, &length) in starts.iter().zip(lengths) { // SAFETY: `preverify` checked every source range. let source = unsafe { values.get_unchecked(start..start + length) }; + // SAFETY: `preverify` checked the summed output length. unsafe { copy_to_spare_unchecked(&mut result, cursor, source) }; cursor += length; } @@ -170,11 +174,16 @@ fn take_preverify_cursor_copy_unchecked( result.freeze() } -fn preverify(source_len: usize, starts: &[usize], lengths: &[usize]) { +fn preverify(source_len: usize, starts: &[usize], lengths: &[usize], output_len: usize) { + assert_eq!(starts.len(), lengths.len()); + let mut cursor = 0usize; for (&start, &length) in starts.iter().zip(lengths) { let end = start.checked_add(length).unwrap(); assert!(end <= source_len); + cursor = cursor.checked_add(length).unwrap(); + assert!(cursor <= output_len); } + assert_eq!(cursor, output_len); } fn copy_to_spare(result: &mut BufferMut, cursor: usize, source: &[u16]) { From 8757db5c7372301dce92a9d98495374ab25abc6f Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 20 Jul 2026 16:02:16 -0400 Subject: [PATCH 3/5] Benchmark advancing PiecewiseSequence copies Signed-off-by: Daniel King --- .../benches/take_slices_to_buffer_matrix.rs | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/vortex-array/benches/take_slices_to_buffer_matrix.rs b/vortex-array/benches/take_slices_to_buffer_matrix.rs index cc81b726684..92f527a04a0 100644 --- a/vortex-array/benches/take_slices_to_buffer_matrix.rs +++ b/vortex-array/benches/take_slices_to_buffer_matrix.rs @@ -4,8 +4,10 @@ //! Microbenchmarks for primitive `take_slices_to_buffer` copy-loop variants. //! //! The matrix covers: -//! - append via `BufferMut::extend_from_slice` vs direct cursor copy into spare output capacity +//! - append via `BufferMut::extend_from_slice`, indexed cursor copy, and advancing pointer copy +//! into spare output capacity //! - ordinary checked slicing vs a preverification pass followed by unchecked slicing +//! - fixed-width short slices at the run counts used by the FSL take benchmarks #![allow(clippy::cast_possible_truncation)] #![expect(clippy::unwrap_used)] @@ -25,6 +27,8 @@ fn main() { const SOURCE_LENS: &[usize] = &[1_000, 10_000, 100_000]; const SLICE_COUNT: usize = 50; +const FIXED_16_SLICE_COUNTS: &[usize] = &[100, 1_000]; +const FIXED_16_SOURCE_RUNS: usize = 500; type TakeSlicesFn = fn(&[u16], &[usize], &[usize], usize) -> Buffer; @@ -48,6 +52,12 @@ fn cursor_copy_safe(bencher: Bencher, source_len: usize) { bench_case(bencher, case, take_cursor_copy_safe); } +#[divan::bench(args = SOURCE_LENS)] +fn advancing_ptr_safe(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_advancing_ptr_safe); +} + #[divan::bench(args = SOURCE_LENS)] fn preverify_extend_unchecked(bencher: Bencher, source_len: usize) { let case = Case::new(source_len); @@ -60,6 +70,30 @@ fn preverify_cursor_copy_unchecked(bencher: Bencher, source_len: usize) { bench_case(bencher, case, take_preverify_cursor_copy_unchecked); } +#[divan::bench(args = SOURCE_LENS)] +fn preverify_advancing_ptr_unchecked(bencher: Bencher, source_len: usize) { + let case = Case::new(source_len); + bench_case(bencher, case, take_preverify_advancing_ptr_unchecked); +} + +#[divan::bench(args = FIXED_16_SLICE_COUNTS)] +fn fixed_16_extend_safe(bencher: Bencher, slice_count: usize) { + let case = Case::fixed_width(slice_count, 16); + bench_case(bencher, case, take_extend_safe); +} + +#[divan::bench(args = FIXED_16_SLICE_COUNTS)] +fn fixed_16_cursor_copy_safe(bencher: Bencher, slice_count: usize) { + let case = Case::fixed_width(slice_count, 16); + bench_case(bencher, case, take_cursor_copy_safe); +} + +#[divan::bench(args = FIXED_16_SLICE_COUNTS)] +fn fixed_16_advancing_ptr_safe(bencher: Bencher, slice_count: usize) { + let case = Case::fixed_width(slice_count, 16); + bench_case(bencher, case, take_advancing_ptr_safe); +} + fn bench_case(bencher: Bencher, case: Case, f: TakeSlicesFn) { bencher .counter(BytesCount::of_many::(case.output_len)) @@ -97,6 +131,22 @@ impl Case { output_len, } } + + fn fixed_width(slice_count: usize, width: usize) -> Self { + let mut rng = StdRng::seed_from_u64(slice_count as u64 ^ 0x05ee_df16); + let source_len = FIXED_16_SOURCE_RUNS * width; + let values = (0..source_len).map(|_| rng.random::()).collect(); + let starts = (0..slice_count) + .map(|_| rng.random_range(0..FIXED_16_SOURCE_RUNS) * width) + .collect(); + let lengths = vec![width; slice_count]; + Self { + values, + starts, + lengths, + output_len: slice_count * width, + } + } } fn take_extend_safe( @@ -133,6 +183,32 @@ fn take_cursor_copy_safe( result.freeze() } +fn take_advancing_ptr_safe( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + let mut result = BufferMut::::with_capacity(output_len); + let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); + let mut remaining = output_len; + for (&start, &length) in starts.iter().zip(lengths) { + let source = &values[start..start + length]; + assert!(length <= remaining); + // SAFETY: `remaining` tracks the writable slots starting at `dst`. + unsafe { + copy_to_uninit(dst, source); + dst = dst.add(length); + } + remaining -= length; + } + assert_eq!(remaining, 0); + + // SAFETY: the loop writes exactly `output_len` values into spare capacity. + unsafe { result.set_len(output_len) }; + result.freeze() +} + fn take_preverify_extend_unchecked( values: &[u16], starts: &[usize], @@ -174,6 +250,30 @@ fn take_preverify_cursor_copy_unchecked( result.freeze() } +fn take_preverify_advancing_ptr_unchecked( + values: &[u16], + starts: &[usize], + lengths: &[usize], + output_len: usize, +) -> Buffer { + preverify(values.len(), starts, lengths, output_len); + + let mut result = BufferMut::::with_capacity(output_len); + let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); + for (&start, &length) in starts.iter().zip(lengths) { + // SAFETY: `preverify` checked every source range and the summed output length. + unsafe { + let source = values.get_unchecked(start..start + length); + copy_to_uninit(dst, source); + dst = dst.add(length); + } + } + + // SAFETY: `preverify` proved that the loop writes exactly `output_len` values. + unsafe { result.set_len(output_len) }; + result.freeze() +} + fn preverify(source_len: usize, starts: &[usize], lengths: &[usize], output_len: usize) { assert_eq!(starts.len(), lengths.len()); let mut cursor = 0usize; From 999589230c4a27cef2580b42c807e6813a4ce926 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 20 Jul 2026 16:29:24 -0400 Subject: [PATCH 4/5] Use shared spare buffer writer in benchmarks Signed-off-by: Daniel King --- .../benches/take_slices_to_buffer_matrix.rs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/vortex-array/benches/take_slices_to_buffer_matrix.rs b/vortex-array/benches/take_slices_to_buffer_matrix.rs index 92f527a04a0..b961f3f0cc1 100644 --- a/vortex-array/benches/take_slices_to_buffer_matrix.rs +++ b/vortex-array/benches/take_slices_to_buffer_matrix.rs @@ -20,6 +20,7 @@ use rand_distr::Distribution; use rand_distr::Normal; use vortex_buffer::Buffer; use vortex_buffer::BufferMut; +use vortex_buffer::SpareBufferWriter; fn main() { divan::main(); @@ -190,22 +191,12 @@ fn take_advancing_ptr_safe( output_len: usize, ) -> Buffer { let mut result = BufferMut::::with_capacity(output_len); - let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); - let mut remaining = output_len; + let mut writer = SpareBufferWriter::new(&mut result, output_len).unwrap(); for (&start, &length) in starts.iter().zip(lengths) { let source = &values[start..start + length]; - assert!(length <= remaining); - // SAFETY: `remaining` tracks the writable slots starting at `dst`. - unsafe { - copy_to_uninit(dst, source); - dst = dst.add(length); - } - remaining -= length; + writer.copy_slice(source).unwrap(); } - assert_eq!(remaining, 0); - - // SAFETY: the loop writes exactly `output_len` values into spare capacity. - unsafe { result.set_len(output_len) }; + writer.finish().unwrap(); result.freeze() } @@ -259,18 +250,15 @@ fn take_preverify_advancing_ptr_unchecked( preverify(values.len(), starts, lengths, output_len); let mut result = BufferMut::::with_capacity(output_len); - let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); + let mut writer = SpareBufferWriter::new(&mut result, output_len).unwrap(); for (&start, &length) in starts.iter().zip(lengths) { // SAFETY: `preverify` checked every source range and the summed output length. unsafe { let source = values.get_unchecked(start..start + length); - copy_to_uninit(dst, source); - dst = dst.add(length); + writer.copy_slice_unchecked(source); } } - - // SAFETY: `preverify` proved that the loop writes exactly `output_len` values. - unsafe { result.set_len(output_len) }; + writer.finish().unwrap(); result.freeze() } From c731e4f6cf147de077244b84a75126e033b689e3 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Tue, 21 Jul 2026 11:09:46 -0400 Subject: [PATCH 5/5] Keep PiecewiseSequence benchmarks sub-millisecond Signed-off-by: Daniel King --- .../piecewise_sequence_take_primitive.rs | 6 ++-- .../benches/take_slices_to_buffer_matrix.rs | 28 ++++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/vortex-array/benches/piecewise_sequence_take_primitive.rs b/vortex-array/benches/piecewise_sequence_take_primitive.rs index 189cfd1da41..718c992f7ea 100644 --- a/vortex-array/benches/piecewise_sequence_take_primitive.rs +++ b/vortex-array/benches/piecewise_sequence_take_primitive.rs @@ -32,9 +32,9 @@ fn main() { static SESSION: LazyLock = LazyLock::new(array_session); -const SOURCE_LEN: usize = 8 * 1024 * 1024; -const OUTPUT_LEN: usize = 1024 * 1024; -const RUN_LENGTHS: &[usize] = &[1, 4, 16, 64, 256, 1024]; +const SOURCE_LEN: usize = 512 * 1024; +const OUTPUT_LEN: usize = 8 * 1024; +const RUN_LENGTHS: &[usize] = &[1, 4, 16, 64, 256]; #[divan::bench(args = RUN_LENGTHS)] fn optimized_constant_lengths(bencher: Bencher, run_length: usize) { diff --git a/vortex-array/benches/take_slices_to_buffer_matrix.rs b/vortex-array/benches/take_slices_to_buffer_matrix.rs index b961f3f0cc1..21cb539ce32 100644 --- a/vortex-array/benches/take_slices_to_buffer_matrix.rs +++ b/vortex-array/benches/take_slices_to_buffer_matrix.rs @@ -20,7 +20,6 @@ use rand_distr::Distribution; use rand_distr::Normal; use vortex_buffer::Buffer; use vortex_buffer::BufferMut; -use vortex_buffer::SpareBufferWriter; fn main() { divan::main(); @@ -191,12 +190,24 @@ fn take_advancing_ptr_safe( output_len: usize, ) -> Buffer { let mut result = BufferMut::::with_capacity(output_len); - let mut writer = SpareBufferWriter::new(&mut result, output_len).unwrap(); + let mut cursor = 0usize; + let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); for (&start, &length) in starts.iter().zip(lengths) { + let end = cursor.checked_add(length).unwrap(); + assert!(end <= output_len); let source = &values[start..start + length]; - writer.copy_slice(source).unwrap(); + // SAFETY: `end <= output_len` proves destination capacity, and safe slicing proves source + // bounds. + unsafe { + copy_to_uninit(dst, source); + dst = dst.add(length); + } + cursor = end; } - writer.finish().unwrap(); + assert_eq!(cursor, output_len); + + // SAFETY: the loop writes exactly `output_len` values into spare capacity. + unsafe { result.set_len(output_len) }; result.freeze() } @@ -250,15 +261,18 @@ fn take_preverify_advancing_ptr_unchecked( preverify(values.len(), starts, lengths, output_len); let mut result = BufferMut::::with_capacity(output_len); - let mut writer = SpareBufferWriter::new(&mut result, output_len).unwrap(); + let mut dst = result.spare_capacity_mut().as_mut_ptr().cast::(); for (&start, &length) in starts.iter().zip(lengths) { // SAFETY: `preverify` checked every source range and the summed output length. unsafe { let source = values.get_unchecked(start..start + length); - writer.copy_slice_unchecked(source); + copy_to_uninit(dst, source); + dst = dst.add(length); } } - writer.finish().unwrap(); + + // SAFETY: `preverify` proves the loop writes exactly `output_len` values into spare capacity. + unsafe { result.set_len(output_len) }; result.freeze() }