This is an automated email from the ASF dual-hosted git repository.
alamb 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 f131b54696 bench: create `zip` kernel benchmarks (#8654)
f131b54696 is described below
commit f131b5469655c2a1afc3b23ce5e3f850d6a389cf
Author: Raz Luvaton <[email protected]>
AuthorDate: Sun Oct 19 22:43:19 2025 +0300
bench: create `zip` kernel benchmarks (#8654)
# Which issue does this PR close?
N/A
# Rationale for this change
I have a PR to improve zip perf for scalar but I don't see any
benchmarks for it:
- #8653
# What changes are included in this PR?
created zip benchmarks for scalar and non scalar with different masks
# Are these changes tested?
N/A
# Are there any user-facing changes?
Nope
---
arrow/Cargo.toml | 5 +
arrow/benches/zip_kernels.rs | 279 +++++++++++++++++++++++++++++++++++++++++++
arrow/src/util/bench_util.rs | 83 ++++++++++++-
3 files changed, 364 insertions(+), 3 deletions(-)
diff --git a/arrow/Cargo.toml b/arrow/Cargo.toml
index c77e85861d..743628c8c7 100644
--- a/arrow/Cargo.toml
+++ b/arrow/Cargo.toml
@@ -177,6 +177,11 @@ name = "interleave_kernels"
harness = false
required-features = ["test_utils"]
+[[bench]]
+name = "zip_kernels"
+harness = false
+required-features = ["test_utils"]
+
[[bench]]
name = "length_kernel"
harness = false
diff --git a/arrow/benches/zip_kernels.rs b/arrow/benches/zip_kernels.rs
new file mode 100644
index 0000000000..5ec9f107d3
--- /dev/null
+++ b/arrow/benches/zip_kernels.rs
@@ -0,0 +1,279 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use criterion::measurement::WallTime;
+use criterion::{BenchmarkGroup, BenchmarkId, Criterion, criterion_group,
criterion_main};
+use rand::distr::{Distribution, StandardUniform};
+use rand::prelude::StdRng;
+use rand::{Rng, SeedableRng};
+use std::hint;
+use std::sync::Arc;
+
+use arrow::array::*;
+use arrow::datatypes::*;
+use arrow::util::bench_util::*;
+use arrow_select::zip::zip;
+
+trait InputGenerator {
+ fn name(&self) -> &str;
+
+ /// Return an ArrayRef containing a single null value
+ fn generate_scalar_with_null_value(&self) -> ArrayRef;
+
+ /// Generate a `number_of_scalars` unique scalars
+ fn generate_non_null_scalars(&self, seed: u64, number_of_scalars: usize)
-> Vec<ArrayRef>;
+
+ /// Generate array with specified length and null percentage
+ fn generate_array(&self, seed: u64, array_length: usize, null_percentage:
f32) -> ArrayRef;
+}
+
+struct GeneratePrimitive<T: ArrowPrimitiveType> {
+ description: String,
+ _marker: std::marker::PhantomData<T>,
+}
+
+impl<T> InputGenerator for GeneratePrimitive<T>
+where
+ T: ArrowPrimitiveType,
+ StandardUniform: Distribution<T::Native>,
+{
+ fn name(&self) -> &str {
+ self.description.as_str()
+ }
+
+ fn generate_scalar_with_null_value(&self) -> ArrayRef {
+ new_null_array(&T::DATA_TYPE, 1)
+ }
+
+ fn generate_non_null_scalars(&self, seed: u64, number_of_scalars: usize)
-> Vec<ArrayRef> {
+ let rng = StdRng::seed_from_u64(seed);
+
+ rng.sample_iter::<T::Native, _>(StandardUniform)
+ .take(number_of_scalars)
+ .map(|v: T::Native| {
+ Arc::new(PrimitiveArray::<T>::new_scalar(v).into_inner()) as
ArrayRef
+ })
+ .collect()
+ }
+
+ fn generate_array(&self, seed: u64, array_length: usize, null_percentage:
f32) -> ArrayRef {
+ Arc::new(create_primitive_array_with_seed::<T>(
+ array_length,
+ null_percentage,
+ seed,
+ ))
+ }
+}
+
+struct GenerateBytes<Byte: ByteArrayType> {
+ range_length: std::ops::Range<usize>,
+ description: String,
+
+ _marker: std::marker::PhantomData<Byte>,
+}
+
+impl<Byte> InputGenerator for GenerateBytes<Byte>
+where
+ Byte: ByteArrayType,
+{
+ fn name(&self) -> &str {
+ self.description.as_str()
+ }
+
+ fn generate_scalar_with_null_value(&self) -> ArrayRef {
+ new_null_array(&Byte::DATA_TYPE, 1)
+ }
+
+ fn generate_non_null_scalars(&self, seed: u64, number_of_scalars: usize)
-> Vec<ArrayRef> {
+ let array = self.generate_array(seed, number_of_scalars, 0.0);
+
+ (0..number_of_scalars).map(|i| array.slice(i, 1)).collect()
+ }
+
+ fn generate_array(&self, seed: u64, array_length: usize, null_percentage:
f32) -> ArrayRef {
+ let is_binary =
+ Byte::DATA_TYPE == DataType::Binary || Byte::DATA_TYPE ==
DataType::LargeBinary;
+ if is_binary {
+ Arc::new(create_binary_array_with_len_range_and_prefix_and_seed::<
+ Byte::Offset,
+ >(
+ array_length,
+ null_percentage,
+ self.range_length.start,
+ self.range_length.end - 1,
+ &[],
+ seed,
+ ))
+ } else {
+ Arc::new(create_string_array_with_len_range_and_prefix_and_seed::<
+ Byte::Offset,
+ >(
+ array_length,
+ null_percentage,
+ self.range_length.start,
+ self.range_length.end - 1,
+ "",
+ seed,
+ ))
+ }
+ }
+}
+
+fn mask_cases(len: usize) -> Vec<(&'static str, BooleanArray)> {
+ vec![
+ ("all_true", create_boolean_array(len, 0.0, 1.0)),
+ ("99pct_true", create_boolean_array(len, 0.0, 0.99)),
+ ("90pct_true", create_boolean_array(len, 0.0, 0.9)),
+ ("50pct_true", create_boolean_array(len, 0.0, 0.5)),
+ ("10pct_true", create_boolean_array(len, 0.0, 0.1)),
+ ("1pct_true", create_boolean_array(len, 0.0, 0.01)),
+ ("all_false", create_boolean_array(len, 0.0, 0.0)),
+ ("50pct_nulls", create_boolean_array(len, 0.5, 0.5)),
+ ]
+}
+
+fn bench_zip_on_input_generator(c: &mut Criterion, input_generator: &impl
InputGenerator) {
+ const ARRAY_LEN: usize = 8192;
+
+ let mut group =
+ c.benchmark_group(format!("zip_{ARRAY_LEN}_from_{}",
input_generator.name()).as_str());
+
+ let null_scalar = input_generator.generate_scalar_with_null_value();
+ let [non_null_scalar_1, non_null_scalar_2]: [_; 2] = input_generator
+ .generate_non_null_scalars(42, 2)
+ .try_into()
+ .unwrap();
+
+ let array_1_10pct_nulls = input_generator.generate_array(42, ARRAY_LEN,
0.1);
+ let array_2_10pct_nulls = input_generator.generate_array(18, ARRAY_LEN,
0.1);
+
+ let masks = mask_cases(ARRAY_LEN);
+
+ // Benchmarks for different scalar combinations
+ for (description, truthy, falsy) in &[
+ ("null_vs_non_null_scalar", &null_scalar, &non_null_scalar_1),
+ (
+ "non_null_scalar_vs_null_scalar",
+ &non_null_scalar_1,
+ &null_scalar,
+ ),
+ ("non_nulls_scalars", &non_null_scalar_1, &non_null_scalar_2),
+ ] {
+ bench_zip_input_on_all_masks(
+ description,
+ &mut group,
+ &masks,
+ &Scalar::new(truthy),
+ &Scalar::new(falsy),
+ );
+ }
+
+ bench_zip_input_on_all_masks(
+ "array_vs_non_null_scalar",
+ &mut group,
+ &masks,
+ &array_1_10pct_nulls,
+ &non_null_scalar_1,
+ );
+
+ bench_zip_input_on_all_masks(
+ "non_null_scalar_vs_array",
+ &mut group,
+ &masks,
+ &array_1_10pct_nulls,
+ &non_null_scalar_1,
+ );
+
+ bench_zip_input_on_all_masks(
+ "array_vs_array",
+ &mut group,
+ &masks,
+ &array_1_10pct_nulls,
+ &array_2_10pct_nulls,
+ );
+
+ group.finish();
+}
+
+fn bench_zip_input_on_all_masks(
+ description: &str,
+ group: &mut BenchmarkGroup<WallTime>,
+ masks: &[(&str, BooleanArray)],
+ truthy: &impl Datum,
+ falsy: &impl Datum,
+) {
+ for (mask_description, mask) in masks {
+ let id = BenchmarkId::new(description, mask_description);
+ group.bench_with_input(id, mask, |b, mask| {
+ b.iter(|| hint::black_box(zip(mask, truthy, falsy)))
+ });
+ }
+}
+
+fn add_benchmark(c: &mut Criterion) {
+ // Primitive
+ bench_zip_on_input_generator(
+ c,
+ &GeneratePrimitive::<Int32Type> {
+ description: "i32".to_string(),
+ _marker: std::marker::PhantomData,
+ },
+ );
+
+ // Short strings
+ bench_zip_on_input_generator(
+ c,
+ &GenerateBytes::<GenericStringType<i32>> {
+ description: "short strings (3..10)".to_string(),
+ range_length: 3..10,
+ _marker: std::marker::PhantomData,
+ },
+ );
+
+ // Long strings
+ bench_zip_on_input_generator(
+ c,
+ &GenerateBytes::<GenericStringType<i32>> {
+ description: "long strings (100..400)".to_string(),
+ range_length: 100..400,
+ _marker: std::marker::PhantomData,
+ },
+ );
+
+ // Short Bytes
+ bench_zip_on_input_generator(
+ c,
+ &GenerateBytes::<GenericBinaryType<i32>> {
+ description: "short bytes (3..10)".to_string(),
+ range_length: 3..10,
+ _marker: std::marker::PhantomData,
+ },
+ );
+
+ // Long Bytes
+ bench_zip_on_input_generator(
+ c,
+ &GenerateBytes::<GenericBinaryType<i32>> {
+ description: "long bytes (100..400)".to_string(),
+ range_length: 100..400,
+ _marker: std::marker::PhantomData,
+ },
+ );
+}
+
+criterion_group!(benches, add_benchmark);
+criterion_main!(benches);
diff --git a/arrow/src/util/bench_util.rs b/arrow/src/util/bench_util.rs
index 4bd648bc40..d85eb4aafd 100644
--- a/arrow/src/util/bench_util.rs
+++ b/arrow/src/util/bench_util.rs
@@ -155,6 +155,27 @@ fn create_string_array_with_len_range_and_prefix<Offset:
OffsetSizeTrait>(
min_str_len: usize,
max_str_len: usize,
prefix: &str,
+) -> GenericStringArray<Offset> {
+ create_string_array_with_len_range_and_prefix_and_seed(
+ size,
+ null_density,
+ min_str_len,
+ max_str_len,
+ prefix,
+ 42,
+ )
+}
+
+/// Creates a random [`GenericStringArray`] of a given `size` and
`null_density`
+/// filling it with random strings with lengths in the specified range,
+/// all starting with the provided `prefix`, generated using the provided
`seed`.
+pub fn create_string_array_with_len_range_and_prefix_and_seed<Offset:
OffsetSizeTrait>(
+ size: usize,
+ null_density: f32,
+ min_str_len: usize,
+ max_str_len: usize,
+ prefix: &str,
+ seed: u64,
) -> GenericStringArray<Offset> {
assert!(
min_str_len <= max_str_len,
@@ -165,7 +186,7 @@ fn create_string_array_with_len_range_and_prefix<Offset:
OffsetSizeTrait>(
"Prefix length must be <= max_str_len"
);
- let rng = &mut seedable_rng();
+ let rng = &mut StdRng::seed_from_u64(seed);
(0..size)
.map(|_| {
if rng.random::<f32>() < null_density {
@@ -449,8 +470,29 @@ pub fn create_binary_array<Offset: OffsetSizeTrait>(
size: usize,
null_density: f32,
) -> GenericBinaryArray<Offset> {
- let rng = &mut seedable_rng();
- let range_rng = &mut seedable_rng();
+ create_binary_array_with_seed(
+ size,
+ null_density,
+ 42, // bytes_seed
+ 42, // bytes_length_seed
+ )
+}
+
+/// Creates a random [`GenericBinaryArray`] of a given `size` and
`null_density`
+/// filling it with random bytes, generated using the provided `seed`s.
+///
+/// the `bytes_seed` is used to seed the RNG for generating the byte values,
+/// while the `bytes_length_seed` is used to seed the RNG for generating the
length of an array item
+///
+/// These values can be the same as they are used to seed different RNGs
internally.
+pub fn create_binary_array_with_seed<Offset: OffsetSizeTrait>(
+ size: usize,
+ null_density: f32,
+ bytes_seed: u64,
+ bytes_length_seed: u64,
+) -> GenericBinaryArray<Offset> {
+ let rng = &mut StdRng::seed_from_u64(bytes_seed);
+ let range_rng = &mut StdRng::seed_from_u64(bytes_length_seed);
(0..size)
.map(|_| {
@@ -467,6 +509,41 @@ pub fn create_binary_array<Offset: OffsetSizeTrait>(
.collect()
}
+/// Creates a random [`GenericBinaryArray`] of a given `size` and
`null_density`
+/// filling it with random bytes with lengths in the specified range,
+/// all starting with the provided `prefix`, generated using the provided
`seed`.
+///
+pub fn create_binary_array_with_len_range_and_prefix_and_seed<Offset:
OffsetSizeTrait>(
+ size: usize,
+ null_density: f32,
+ min_len: usize,
+ max_len: usize,
+ prefix: &[u8],
+ seed: u64,
+) -> GenericBinaryArray<Offset> {
+ assert!(min_len <= max_len, "min_len must be <= max_len");
+ assert!(prefix.len() <= max_len, "Prefix length must be <= max_len");
+
+ let rng = &mut StdRng::seed_from_u64(seed);
+ (0..size)
+ .map(|_| {
+ if rng.random::<f32>() < null_density {
+ None
+ } else {
+ let remaining_len = rng
+
.random_range(min_len.saturating_sub(prefix.len())..=(max_len - prefix.len()));
+
+ let remaining = rng
+ .sample_iter::<u8, _>(StandardUniform)
+ .take(remaining_len);
+
+ let value =
prefix.iter().copied().chain(remaining).collect::<Vec<u8>>();
+ Some(value)
+ }
+ })
+ .collect()
+}
+
/// Creates an random (but fixed-seeded) array of a given size and null density
pub fn create_fsb_array(size: usize, null_density: f32, value_len: usize) ->
FixedSizeBinaryArray {
let rng = &mut seedable_rng();