From f0d133b131e86040b0fc6a4d12e814a945cdc319 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sat, 1 Aug 2026 13:27:05 -0400 Subject: [PATCH 1/3] Add scalar function performance baselines Signed-off-by: Connor Tsui --- .github/workflows/codspeed.yml | 1 + Cargo.lock | 3 + vortex-array/Cargo.toml | 4 + vortex-array/benches/binary_ops.rs | 24 ++ vortex-array/benches/byte_length.rs | 98 ++++++ vortex-array/benches/like.rs | 81 +++-- vortex-geo/Cargo.toml | 9 + vortex-geo/benches/binary_predicates.rs | 387 +++++++++++++++++++++ vortex-geo/benches/distance.rs | 144 ++++++++ vortex-geo/benches/envelope.rs | 13 +- vortex-geo/benches/predicate_bbox.rs | 5 +- vortex-tensor/Cargo.toml | 18 + vortex-tensor/benches/cosine_similarity.rs | 128 +++++++ vortex-tensor/benches/inner_product.rs | 86 +++++ vortex-tensor/benches/l2_norm.rs | 82 +++++ vortex-tensor/benches/normalized.rs | 95 +++++ 16 files changed, 1155 insertions(+), 23 deletions(-) create mode 100644 vortex-array/benches/byte_length.rs create mode 100644 vortex-geo/benches/binary_predicates.rs create mode 100644 vortex-geo/benches/distance.rs create mode 100644 vortex-tensor/benches/cosine_similarity.rs create mode 100644 vortex-tensor/benches/inner_product.rs create mode 100644 vortex-tensor/benches/l2_norm.rs create mode 100644 vortex-tensor/benches/normalized.rs diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 5bc486c6623..aeafc73ed0e 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -57,6 +57,7 @@ jobs: - { shard: 6, name: "Encodings 3", packages: "vortex-pco vortex-runend vortex-sequence" } - { shard: 7, name: "Encodings 4", packages: "vortex-sparse vortex-zigzag vortex-zstd" } - { shard: 8, name: "Storage formats & row encoding", packages: "vortex-flatbuffers vortex-proto vortex-btrblocks vortex-row" } + - { shard: 9, name: "Tensor & geo", packages: "vortex-tensor vortex-geo" } name: "Benchmark with Codspeed (Shard #${{ matrix.shard }})" timeout-minutes: 30 runs-on: >- diff --git a/Cargo.lock b/Cargo.lock index 6b69e6f253a..53202930d5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10088,6 +10088,7 @@ dependencies = [ "geo-types", "geoarrow", "geoarrow-cast", + "mimalloc", "prost 0.14.4", "rstest", "vortex-array", @@ -10501,8 +10502,10 @@ version = "0.1.0" dependencies = [ "arrow-array 58.4.0", "arrow-schema 58.4.0", + "codspeed-divan-compat", "half", "itertools 0.14.0", + "mimalloc", "num-traits", "prost 0.14.4", "rstest", diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index 70fba4acdcf..4bcab93198a 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -137,6 +137,10 @@ harness = false name = "like" harness = false +[[bench]] +name = "byte_length" +harness = false + [[bench]] name = "interleave" harness = false diff --git a/vortex-array/benches/binary_ops.rs b/vortex-array/benches/binary_ops.rs index f3792e22990..00ad82d3457 100644 --- a/vortex-array/benches/binary_ops.rs +++ b/vortex-array/benches/binary_ops.rs @@ -54,6 +54,30 @@ fn add_i64_nullable(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[divan::bench] +fn add_i64_constant(bencher: Bencher) { + let lhs = primitive_nonnull(0).into_array(); + let rhs = ConstantArray::new(1_000_000i64, LEN).into_array(); + + bench_primitive(bencher, lhs, rhs, Operator::Add); +} + +#[divan::bench] +fn add_i32_nonnull(bencher: Bencher) { + let lhs = primitive_i32_small_nonnull(1).into_array(); + let rhs = primitive_i32_small_nonnull(17).into_array(); + + bench_primitive(bencher, lhs, rhs, Operator::Add); +} + +#[divan::bench] +fn add_u32_nonnull(bencher: Bencher) { + let lhs = primitive_u32_small_nonnull(1).into_array(); + let rhs = primitive_u32_small_nonnull(17).into_array(); + + bench_primitive(bencher, lhs, rhs, Operator::Add); +} + #[divan::bench] fn mul_i64_nonnull(bencher: Bencher) { let lhs = primitive_small_nonnull(1).into_array(); diff --git a/vortex-array/benches/byte_length.rs b/vortex-array/benches/byte_length.rs new file mode 100644 index 00000000000..e64cb98b562 --- /dev/null +++ b/vortex-array/benches/byte_length.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Baseline throughput for `byte_length` over UTF-8 view arrays. +//! +//! The arms cover inline and out-of-line views, plus nullable out-of-line views. Their names are +//! intended to remain stable across scalar-function implementation changes so CodSpeed can compare +//! them against `develop`. + +#![expect(clippy::unwrap_used)] + +use std::sync::LazyLock; + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::Canonical; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::array_session; +use vortex_array::arrays::MaskedArray; +use vortex_array::arrays::ScalarFnArray; +use vortex_array::arrays::VarBinViewArray; +use vortex_array::scalar_fn::EmptyOptions; +use vortex_array::scalar_fn::TypedScalarFnInstance; +use vortex_array::scalar_fn::fns::byte_length::ByteLength; +use vortex_array::validity::Validity; +use vortex_session::VortexSession; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +static SESSION: LazyLock = LazyLock::new(array_session); + +fn main() { + LazyLock::force(&SESSION); + divan::main(); +} + +const SIZES: &[usize] = &[4_096, 65_536]; + +fn long_strings(len: usize) -> ArrayRef { + VarBinViewArray::from_iter_str((0..len).map(|i| format!("a string well past inline: {i}"))) + .into_array() +} + +fn short_strings(len: usize) -> ArrayRef { + VarBinViewArray::from_iter_str((0..len).map(|i| format!("{}", i % 1_000))).into_array() +} + +fn bench_byte_length(bencher: Bencher, input: ArrayRef) { + let len = input.len(); + bencher + .counter(ItemsCount::new(len)) + .with_inputs(|| { + ( + ScalarFnArray::try_new( + TypedScalarFnInstance::new(ByteLength, EmptyOptions).erased(), + vec![input.clone()], + ) + .unwrap() + .into_array(), + SESSION.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +#[divan::bench(args = SIZES)] +fn inline(bencher: Bencher, len: usize) { + bench_byte_length(bencher, short_strings(len)); +} + +#[divan::bench(args = SIZES)] +fn out_of_line(bencher: Bencher, len: usize) { + bench_byte_length(bencher, long_strings(len)); +} + +#[divan::bench(args = SIZES)] +fn nullable_out_of_line(bencher: Bencher, len: usize) { + let validity = Validity::from_iter((0..len).map(|i| i % 8 != 0)); + let input = MaskedArray::try_new(long_strings(len), validity) + .unwrap() + .into_array(); + bench_byte_length(bencher, input); +} + +#[divan::bench(args = SIZES)] +fn nullable_out_of_line_90pct(bencher: Bencher, len: usize) { + let validity = Validity::from_iter((0..len).map(|i| i.is_multiple_of(10))); + let input = MaskedArray::try_new(long_strings(len), validity) + .unwrap() + .into_array(); + bench_byte_length(bencher, input); +} diff --git a/vortex-array/benches/like.rs b/vortex-array/benches/like.rs index 68219724717..1dc1f2c240b 100644 --- a/vortex-array/benches/like.rs +++ b/vortex-array/benches/like.rs @@ -13,8 +13,9 @@ use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::arrays::BoolArray; use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::ScalarFnArray; use vortex_array::arrays::VarBinViewArray; -use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; +use vortex_array::scalar_fn::TypedScalarFnInstance; use vortex_array::scalar_fn::fns::like::Like; use vortex_array::scalar_fn::fns::like::LikeOptions; @@ -24,11 +25,16 @@ fn main() { const ARRAY_SIZE: usize = 2_048; +/// The per-row pattern arms compile one pattern per row in the worst case, which costs an order of +/// magnitude more per row than matching against a constant pattern. They use a smaller fixture to +/// stay inside the 1 ms per-iteration budget from `docs/developer-guide/benchmarking.md`. +const PER_ROW_PATTERN_ROWS: usize = 512; + /// Random lowercase strings of 4..=24 bytes, some with a `hello` infix. -fn strings() -> ArrayRef { +fn strings(len: usize) -> ArrayRef { let mut rng = StdRng::seed_from_u64(0); let len_dist = Uniform::new_inclusive(4usize, 24).unwrap(); - VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|i| { + VarBinViewArray::from_iter_str((0..len).map(|i| { let len = rng.sample(len_dist); let mut s: String = (0..len) .map(|_| char::from(rng.random_range(b'a'..=b'z'))) @@ -43,19 +49,19 @@ fn strings() -> ArrayRef { fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) { let session = vortex_array::array_session(); - let array = strings(); + let array = strings(ARRAY_SIZE); bencher .with_inputs(|| { ( - Like.try_new_array( - ARRAY_SIZE, - options, - [ + ScalarFnArray::try_new( + TypedScalarFnInstance::new(Like, options).erased(), + vec![ array.clone(), ConstantArray::new(pattern, ARRAY_SIZE).into_array(), ], ) - .unwrap(), + .unwrap() + .into_array(), session.create_execution_ctx(), ) }) @@ -87,28 +93,63 @@ fn like_regex(bencher: Bencher) { bench_like(bencher, "h_llo%w%d", LikeOptions::default()); } -#[divan::bench] -fn like_per_row_patterns(bencher: Bencher) { +fn bench_per_row_patterns(bencher: Bencher, patterns: ArrayRef) { let session = vortex_array::array_session(); - let array = strings(); - // A non-constant pattern child takes the per-row path; repeated patterns hit the - // compile cache. - let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array(); + let array = strings(patterns.len()); bencher .with_inputs(|| { ( - Like.try_new_array( - ARRAY_SIZE, - LikeOptions::default(), - [array.clone(), patterns.clone()], + ScalarFnArray::try_new( + TypedScalarFnInstance::new(Like, LikeOptions::default()).erased(), + vec![array.clone(), patterns.clone()], ) - .unwrap(), + .unwrap() + .into_array(), session.create_execution_ctx(), ) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } +#[divan::bench] +fn like_per_row_patterns(bencher: Bencher) { + // A non-constant pattern child takes the per-row path; repeated patterns hit the compile cache. + let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array(); + bench_per_row_patterns(bencher, patterns); +} + +/// The cached half of the compile-cache pair: every row repeats one pattern whose five-byte shape +/// matches the distinct-pattern arm, so the two differ only in whether the cache hits. Both use +/// [`PER_ROW_PATTERN_ROWS`] rows to stay comparable. +/// +/// [`like_per_row_distinct_patterns`] +#[divan::bench] +fn like_per_row_repeated_patterns(bencher: Bencher) { + let patterns = + VarBinViewArray::from_iter_str((0..PER_ROW_PATTERN_ROWS).map(|_| "%aaa%")).into_array(); + bench_per_row_patterns(bencher, patterns); +} + +/// The per-row path with the compile cache defeated: every row carries a distinct pattern of the +/// same shape as the repeated-pattern arm, so each row pays one pattern compilation. +/// +/// [`like_per_row_repeated_patterns`] +#[divan::bench] +fn like_per_row_distinct_patterns(bencher: Bencher) { + let patterns = VarBinViewArray::from_iter_str( + (0..PER_ROW_PATTERN_ROWS).map(|i| format!("%{}%", distinct_trigram(i))), + ) + .into_array(); + bench_per_row_patterns(bencher, patterns); +} + +/// A distinct three-letter lowercase infix per row, so every pattern has the same shape while +/// no two rows share a compiled pattern. +fn distinct_trigram(i: usize) -> String { + let letter = |shift: usize| char::from(b'a' + u8::try_from((i >> shift) % 26).unwrap()); + [letter(0), letter(5), letter(10)].iter().collect() +} + #[divan::bench] fn ilike_contains(bencher: Bencher) { bench_like( diff --git a/vortex-geo/Cargo.toml b/vortex-geo/Cargo.toml index fcd13ffa641..6eddab411d9 100644 --- a/vortex-geo/Cargo.toml +++ b/vortex-geo/Cargo.toml @@ -36,6 +36,7 @@ _test-harness = [] [dev-dependencies] divan = { workspace = true } +mimalloc = { workspace = true } rstest = { workspace = true } vortex-array = { workspace = true, features = ["_test-harness"] } vortex-geo = { path = ".", features = ["_test-harness"] } @@ -49,5 +50,13 @@ harness = false name = "predicate_bbox" harness = false +[[bench]] +name = "binary_predicates" +harness = false + +[[bench]] +name = "distance" +harness = false + [lints] workspace = true diff --git a/vortex-geo/benches/binary_predicates.rs b/vortex-geo/benches/binary_predicates.rs new file mode 100644 index 00000000000..af934724a4a --- /dev/null +++ b/vortex-geo/benches/binary_predicates.rs @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Microbenchmarks for the binary geometry predicates `ST_Contains` and `ST_Intersects`, focused +//! on the cost of a batch-constant operand. +//! +//! The constant is a 128-vertex query polygon, the shape a spatial filter broadcasts against a +//! column. Arms pair it with a point column (geo answers those pairings with direct +//! point-in-polygon algorithms) and with a small-polygon column (geo routes those pairings through +//! bounding-box prechecks and `relate`), covering both a mostly-disjoint dataset where the bbox +//! early-out rejects nearly every row and an all-overlapping one where it never does. The +//! column-x-column arms are the control: no operand is constant, so a prepared path has nothing to +//! hoist and must not regress them. +//! +//! `contains` has no all-overlapping arm. One `contains(query polygon, contained square)` row +//! builds a topology graph over the constant's 128 edges, which CodSpeed's CPU simulation charges +//! around 120 µs, so no row count both fits the per-iteration budget and exercises the row loop. +//! [`intersects::polygons_overlapping_x_constant`] covers the never-rejects case instead. +//! +//! Run with `cargo bench -p vortex-geo --bench binary_predicates`. + +#![expect(clippy::unwrap_used)] + +use std::f64::consts::TAU; +use std::sync::LazyLock; + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::Canonical; +use vortex_array::ExecutionCtx; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::MaskedArray; +use vortex_array::validity::Validity; +use vortex_error::VortexResult; +use vortex_geo::scalar_fn::contains::GeoContains; +use vortex_geo::scalar_fn::intersects::GeoIntersects; +use vortex_geo::test_harness::geo_session; +use vortex_geo::test_harness::point_column; +use vortex_geo::test_harness::polygon_column; +use vortex_session::VortexSession; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn main() { + divan::main(); +} + +static SESSION: LazyLock = LazyLock::new(geo_session); + +/// The ordinary arms use the same row count so results are comparable across shapes. A geometry +/// predicate costs roughly a microsecond per row under CodSpeed's CPU simulation, which caps the +/// row count well below a typical batch to stay inside the 1 ms per-iteration budget from +/// `docs/developer-guide/benchmarking.md`. +const ROWS: usize = 1 << 8; + +/// The all-overlapping polygon arm never rejects on bounding boxes, so every row pays for the full +/// pairwise predicate. It needs a smaller fixture than [`ROWS`] to stay inside the same budget. +const OVERLAPPING_POLYGON_ROWS: usize = 1 << 6; + +/// Deterministic pseudo-random value in `[0, 1)`. +fn unit(i: usize) -> f64 { + ((i.wrapping_mul(2654435761) >> 8) % 10_000) as f64 / 10_000.0 +} + +/// The exterior ring of a convex 128-gon of radius 100 centered at `(cx, cy)`: enough vertices +/// that per-row work proportional to the constant's size shows up clearly. +fn query_ring(cx: f64, cy: f64) -> Vec<(f64, f64)> { + let n = 128; + (0..=n) + .map(|i| { + let theta = (i % n) as f64 / n as f64 * TAU; + (cx + 100.0 * theta.cos(), cy + 100.0 * theta.sin()) + }) + .collect() +} + +/// The query polygon as a batch-constant operand: a top-level `ConstantArray` over the geometry +/// extension scalar, the shape that reaches the row loop's stride-0 path. +fn query_constant(ctx: &mut ExecutionCtx, rows: usize) -> ArrayRef { + let scalar = polygon_column(vec![vec![query_ring(0.0, 0.0)]]) + .unwrap() + .execute_scalar(0, ctx) + .unwrap(); + ConstantArray::new(scalar, rows).into_array() +} + +/// A batch-constant point operand with the requested row count. +fn point_constant(ctx: &mut ExecutionCtx, rows: usize) -> ArrayRef { + let scalar = point_column(vec![0.0], vec![0.0]) + .unwrap() + .execute_scalar(0, ctx) + .unwrap(); + ConstantArray::new(scalar, rows).into_array() +} + +/// A small square (side 2) centered at `(cx, cy)`. +fn square(cx: f64, cy: f64) -> Vec> { + vec![vec![ + (cx - 1.0, cy - 1.0), + (cx + 1.0, cy - 1.0), + (cx + 1.0, cy + 1.0), + (cx - 1.0, cy + 1.0), + (cx - 1.0, cy - 1.0), + ]] +} + +/// `rows` small squares whose centers avoid the query polygon almost always: the shape of a +/// selective spatial filter, where a bbox check rejects nearly every row. +fn squares_mostly_disjoint(rows: usize) -> ArrayRef { + let rows = (0..rows) + .map(|i| square(150.0 + 700.0 * unit(i), 150.0 + 700.0 * unit(i + 1))) + .collect(); + polygon_column(rows).unwrap() +} + +/// `rows` small squares whose centers all fall well inside the query polygon, so a bbox check +/// never rejects and the full pairwise predicate always runs. +fn squares_mostly_overlapping(rows: usize) -> ArrayRef { + let rows = (0..rows) + .map(|i| square(120.0 * unit(i) - 60.0, 120.0 * unit(i + 1) - 60.0)) + .collect(); + polygon_column(rows).unwrap() +} + +/// `rows` points spread over `[-150, 150)^2`, mixing rows inside and outside the query polygon. +fn points(rows: usize) -> ArrayRef { + let xs = (0..rows).map(|i| 300.0 * unit(i) - 150.0).collect(); + let ys = (0..rows).map(|i| 300.0 * unit(i + 1) - 150.0).collect(); + point_column(xs, ys).unwrap() +} + +/// Execute `array` to completion. +fn execute(array: VortexResult, ctx: &mut ExecutionCtx) -> ArrayRef { + array + .unwrap() + .into_array() + .execute::(ctx) + .unwrap() + .into_array() +} + +/// Marks one row in `null_every` null without changing the geometry storage. +fn nullable_every(array: ArrayRef, null_every: usize) -> ArrayRef { + let validity = Validity::from_iter((0..array.len()).map(|i| !i.is_multiple_of(null_every))); + MaskedArray::try_new(array, validity).unwrap().into_array() +} + +/// A deterministic 90%-null validity pattern. +fn ninety_percent_null(array: ArrayRef) -> ArrayRef { + let validity = Validity::from_iter((0..array.len()).map(|i| i.is_multiple_of(10))); + MaskedArray::try_new(array, validity).unwrap().into_array() +} + +/// A deterministic 50% validity pattern with a distinct phase per operand. Adjacent phases make +/// the two columns jointly cover every valid/null combination once per four rows. +fn half_valid(array: ArrayRef, phase: usize) -> ArrayRef { + let validity = Validity::from_iter((0..array.len()).map(|i| (i + phase) % 4 < 2)); + MaskedArray::try_new(array, validity).unwrap().into_array() +} + +mod contains { + use super::*; + + /// Control: no constant operand, direct point-in-polygon per row. + #[divan::bench] + fn column_x_column_points(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = squares_mostly_overlapping(ROWS); + let points = points(ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(polygons.clone(), points.clone()), + &mut ctx, + ) + }); + } + + /// Control: no constant operand, relate-routed polygon pairs per row. + #[divan::bench] + fn column_x_column_polygons(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let a = squares_mostly_overlapping(ROWS); + let b = squares_mostly_disjoint(ROWS); + bencher + .counter(ItemsCount::new(ROWS)) + .bench_local(|| execute(GeoContains::try_new_array(a.clone(), b.clone()), &mut ctx)); + } + + /// Constant container against a point column: geo's direct point-in-polygon pairing. + #[divan::bench] + fn constant_x_points(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let query = query_constant(&mut ctx, ROWS); + let points = points(ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(query.clone(), points.clone()), + &mut ctx, + ) + }); + } + + /// Constant container against mostly-disjoint polygons: relate-routed, and almost every row + /// short-circuits on bounding boxes inside relate. + #[divan::bench] + fn constant_x_polygons_disjoint(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let query = query_constant(&mut ctx, ROWS); + let polygons = squares_mostly_disjoint(ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(query.clone(), polygons.clone()), + &mut ctx, + ) + }); + } + + /// Constant container against a point column with one null row in eight. + #[divan::bench] + fn constant_x_nullable_points(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let query = query_constant(&mut ctx, ROWS); + let points = nullable_every(points(ROWS), 8); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(query.clone(), points.clone()), + &mut ctx, + ) + }); + } + + /// Constant container against mostly-disjoint polygons with one null row in eight. + #[divan::bench] + fn constant_x_nullable_polygons_disjoint(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let query = query_constant(&mut ctx, ROWS); + let polygons = nullable_every(squares_mostly_disjoint(ROWS), 8); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(query.clone(), polygons.clone()), + &mut ctx, + ) + }); + } + + /// Polygon columns against a constant point exercise the direct geometry pairing without a + /// constant container. + #[divan::bench] + fn polygons_x_constant_point(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = squares_mostly_overlapping(ROWS); + let point = point_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(polygons.clone(), point.clone()), + &mut ctx, + ) + }); + } + + /// The same polygon-x-constant-point pairing with a deterministic 90% null polygon column. + #[divan::bench] + fn nullable_polygons_90pct_x_constant_point(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = ninety_percent_null(squares_mostly_overlapping(ROWS)); + let point = point_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(polygons.clone(), point.clone()), + &mut ctx, + ) + }); + } + + /// Independently 50%-valid polygon and point columns cover mixed validity without test-only + /// execution controls. + #[divan::bench] + fn nullable_polygons_x_nullable_points(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = half_valid(squares_mostly_overlapping(ROWS), 0); + let points = half_valid(points(ROWS), 1); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoContains::try_new_array(polygons.clone(), points.clone()), + &mut ctx, + ) + }); + } +} + +mod intersects { + use super::*; + + /// Control: no constant operand, polygon pairs per row. + #[divan::bench] + fn column_x_column_polygons(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let a = squares_mostly_overlapping(ROWS); + let b = squares_mostly_disjoint(ROWS); + bencher + .counter(ItemsCount::new(ROWS)) + .bench_local(|| execute(GeoIntersects::try_new_array(a.clone(), b.clone()), &mut ctx)); + } + + /// Point column against the constant query: geo answers point-x-polygon directly, with no + /// bbox precheck to hoist. + #[divan::bench] + fn points_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let points = points(ROWS); + let query = query_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoIntersects::try_new_array(points.clone(), query.clone()), + &mut ctx, + ) + }); + } + + /// Mostly-disjoint polygons against the constant query: the bbox precheck rejects nearly + /// every row, so the constant's per-row bounding-box fold dominates the baseline. + #[divan::bench] + fn polygons_disjoint_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = squares_mostly_disjoint(ROWS); + let query = query_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoIntersects::try_new_array(polygons.clone(), query.clone()), + &mut ctx, + ) + }); + } + + /// Mostly-overlapping polygons against the constant query: the bbox precheck never rejects, so + /// every row still pays for the full pairwise predicate. That is several times the per-row cost + /// of the other arms, so this one uses [`OVERLAPPING_POLYGON_ROWS`]. + #[divan::bench] + fn polygons_overlapping_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = squares_mostly_overlapping(OVERLAPPING_POLYGON_ROWS); + let query = query_constant(&mut ctx, OVERLAPPING_POLYGON_ROWS); + bencher + .counter(ItemsCount::new(OVERLAPPING_POLYGON_ROWS)) + .bench_local(|| { + execute( + GeoIntersects::try_new_array(polygons.clone(), query.clone()), + &mut ctx, + ) + }); + } + + /// Nullable point column against the constant query, with one null row in eight. + #[divan::bench] + fn nullable_points_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let points = nullable_every(points(ROWS), 8); + let query = query_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoIntersects::try_new_array(points.clone(), query.clone()), + &mut ctx, + ) + }); + } + + /// Mostly-disjoint nullable polygons against the constant query. + #[divan::bench] + fn nullable_polygons_disjoint_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = nullable_every(squares_mostly_disjoint(ROWS), 8); + let query = query_constant(&mut ctx, ROWS); + bencher.counter(ItemsCount::new(ROWS)).bench_local(|| { + execute( + GeoIntersects::try_new_array(polygons.clone(), query.clone()), + &mut ctx, + ) + }); + } +} diff --git a/vortex-geo/benches/distance.rs b/vortex-geo/benches/distance.rs new file mode 100644 index 00000000000..96e143359de --- /dev/null +++ b/vortex-geo/benches/distance.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Production baselines for planar `ST_Distance` over native geometry columns. +//! +//! Row counts are set by the 1 ms per-iteration budget from `docs/developer-guide/benchmarking.md`, +//! measured against CodSpeed's CPU simulation rather than local wall clock. Polygon distance +//! rebuilds both geometry R-trees per call, so it costs an order of magnitude more per row than +//! point distance and takes a correspondingly smaller fixture. + +#![expect(clippy::unwrap_used)] + +use std::sync::LazyLock; + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::Canonical; +use vortex_array::ExecutionCtx; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::MaskedArray; +use vortex_array::validity::Validity; +use vortex_error::VortexResult; +use vortex_geo::scalar_fn::distance::GeoDistance; +use vortex_geo::test_harness::geo_session; +use vortex_geo::test_harness::point_column; +use vortex_geo::test_harness::polygon_column; +use vortex_session::VortexSession; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +static SESSION: LazyLock = LazyLock::new(geo_session); + +const POINT_ROWS: usize = 1_024; +const POLYGON_ROWS: usize = 64; + +fn main() { + divan::main(); +} + +/// Deterministic pseudo-random value in `[0, 1)`. +fn unit(i: usize) -> f64 { + ((i.wrapping_mul(2654435761) >> 8) % 10_000) as f64 / 10_000.0 +} + +fn points(rows: usize, offset: usize) -> ArrayRef { + let xs = (0..rows) + .map(|i| 300.0 * unit(i + offset) - 150.0) + .collect(); + let ys = (0..rows) + .map(|i| 300.0 * unit(i + offset + 1) - 150.0) + .collect(); + point_column(xs, ys).unwrap() +} + +/// A small square centered at `(cx, cy)`. +fn square(cx: f64, cy: f64) -> Vec> { + vec![vec![ + (cx - 1.0, cy - 1.0), + (cx + 1.0, cy - 1.0), + (cx + 1.0, cy + 1.0), + (cx - 1.0, cy + 1.0), + (cx - 1.0, cy - 1.0), + ]] +} + +fn polygons(rows: usize) -> ArrayRef { + let rows = (0..rows) + .map(|i| square(150.0 + 700.0 * unit(i), 150.0 + 700.0 * unit(i + 1))) + .collect(); + polygon_column(rows).unwrap() +} + +fn point_constant(x: f64, y: f64, rows: usize, ctx: &mut ExecutionCtx) -> ArrayRef { + let scalar = point_column(vec![x], vec![y]) + .unwrap() + .execute_scalar(0, ctx) + .unwrap(); + ConstantArray::new(scalar, rows).into_array() +} + +fn polygon_constant(rows: usize, ctx: &mut ExecutionCtx) -> ArrayRef { + let scalar = polygon_column(vec![square(0.0, 0.0)]) + .unwrap() + .execute_scalar(0, ctx) + .unwrap(); + ConstantArray::new(scalar, rows).into_array() +} + +fn execute(distance: VortexResult, ctx: &mut ExecutionCtx) -> ArrayRef { + distance + .unwrap() + .into_array() + .execute::(ctx) + .unwrap() + .into_array() +} + +fn bench_distance(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { + let rows = lhs.len(); + let mut ctx = SESSION.create_execution_ctx(); + bencher.counter(ItemsCount::new(rows)).bench_local(|| { + execute( + GeoDistance::try_new_array(lhs.clone(), rhs.clone()), + &mut ctx, + ) + }); +} + +#[divan::bench] +fn point_column_x_point_column(bencher: Bencher) { + bench_distance(bencher, points(POINT_ROWS, 0), points(POINT_ROWS, 97)); +} + +#[divan::bench] +fn point_column_x_constant_point(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let point = point_constant(0.0, 0.0, POINT_ROWS, &mut ctx); + bench_distance(bencher, points(POINT_ROWS, 0), point); +} + +#[divan::bench] +fn polygon_column_x_constant_polygon(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygon = polygon_constant(POLYGON_ROWS, &mut ctx); + bench_distance(bencher, polygons(POLYGON_ROWS), polygon); +} + +#[divan::bench] +fn nullable_point_column_x_constant_point(bencher: Bencher) { + let validity = Validity::from_iter((0..POINT_ROWS).map(|i| !i.is_multiple_of(8))); + let points = MaskedArray::try_new(points(POINT_ROWS, 0), validity) + .unwrap() + .into_array(); + let mut ctx = SESSION.create_execution_ctx(); + let point = point_constant(0.0, 0.0, POINT_ROWS, &mut ctx); + bench_distance(bencher, points, point); +} diff --git a/vortex-geo/benches/envelope.rs b/vortex-geo/benches/envelope.rs index 6ae5d6404c2..f15516437b5 100644 --- a/vortex-geo/benches/envelope.rs +++ b/vortex-geo/benches/envelope.rs @@ -20,6 +20,7 @@ use std::sync::LazyLock; use divan::Bencher; use divan::counter::ItemsCount; +use mimalloc::MiMalloc; use vortex_array::ArrayRef; use vortex_array::Canonical; use vortex_array::ExecutionCtx; @@ -35,6 +36,11 @@ use vortex_geo::test_harness::nullable_point_column; use vortex_geo::test_harness::point_column; use vortex_session::VortexSession; +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + fn main() { divan::main(); } @@ -42,8 +48,11 @@ fn main() { static SESSION: LazyLock = LazyLock::new(geo_session); /// Every case has the same row count so results are comparable across shapes: differences then -/// reflect per-row cost (nesting depth, validity handling) rather than input size. -const ROWS: usize = 1 << 17; +/// reflect per-row cost (nesting depth, validity handling) rather than input size. The nested cases +/// carry 32 vertices per row, and CodSpeed's CPU simulation charges their memory traffic far more +/// than a desktop does, so this is what keeps them inside the 1 ms per-iteration budget from +/// `docs/developer-guide/benchmarking.md`. +const ROWS: usize = 1 << 11; /// Deterministic pseudo-random ordinate in `[0, 1000)`. fn ordinate(i: usize) -> f64 { diff --git a/vortex-geo/benches/predicate_bbox.rs b/vortex-geo/benches/predicate_bbox.rs index d0d6d292366..74dcca16b48 100644 --- a/vortex-geo/benches/predicate_bbox.rs +++ b/vortex-geo/benches/predicate_bbox.rs @@ -26,7 +26,10 @@ fn main() { divan::main(); } -const ROWS: usize = 1 << 13; +/// A candidate row runs the exact predicate against a 128-vertex query polygon, which CodSpeed's +/// CPU simulation charges far more than a desktop does. This row count keeps the candidate arms +/// inside the 1 ms per-iteration budget from `docs/developer-guide/benchmarking.md`. +const ROWS: usize = 1 << 9; const VERTICES: usize = 128; static QUERY: LazyLock> = LazyLock::new(|| { diff --git a/vortex-tensor/Cargo.toml b/vortex-tensor/Cargo.toml index abe63fde595..abdca676775 100644 --- a/vortex-tensor/Cargo.toml +++ b/vortex-tensor/Cargo.toml @@ -32,7 +32,25 @@ num-traits = { workspace = true } prost = { workspace = true } [dev-dependencies] +divan = { workspace = true } +mimalloc = { workspace = true } rstest = { workspace = true } vortex-array = { workspace = true, features = ["_test-harness"] } vortex-btrblocks = { workspace = true } vortex-mask = { workspace = true } + +[[bench]] +name = "cosine_similarity" +harness = false + +[[bench]] +name = "l2_norm" +harness = false + +[[bench]] +name = "normalized" +harness = false + +[[bench]] +name = "inner_product" +harness = false diff --git a/vortex-tensor/benches/cosine_similarity.rs b/vortex-tensor/benches/cosine_similarity.rs new file mode 100644 index 00000000000..feb860f3372 --- /dev/null +++ b/vortex-tensor/benches/cosine_similarity.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Baseline throughput for cosine similarity over tensor columns. +//! +//! The arms cover pairwise columns and both representations of a broadcast query vector. These +//! names are intended to remain stable across scalar-function implementation changes so CodSpeed +//! can compare them against `develop`. +//! +//! Rows are derived from a fixed element budget rather than fixed per arm, so widening a vector +//! trades rows for elements instead of multiplying the work. See [`ELEMENTS`]. + +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::EmptyMetadata; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::ExtensionArray; +use vortex_array::arrays::FixedSizeListArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::dtype::DType; +use vortex_array::dtype::Nullability; +use vortex_array::dtype::PType; +use vortex_array::scalar::Scalar; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_tensor::scalar_fns::cosine_similarity::CosineSimilarity; +use vortex_tensor::vector::Vector; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn main() { + divan::main(); +} + +/// Total `f64` elements per operand, held constant across widths: the row count is +/// `ELEMENTS / width`. This budget is a quarter of the one the other tensor benches use, because +/// the constant arms recompute the broadcast vector's norm per row and cost roughly ten times the +/// column arms per element. It is what keeps every arm inside the 1 ms per-iteration limit from +/// `docs/developer-guide/benchmarking.md`, measured against CodSpeed's CPU simulation. +const ELEMENTS: usize = 4_096; + +/// Widths chosen to separate the two costs, as in `l2_norm.rs`: the redundant norm pass is +/// `O(rows * width)`, one third of the closure's arithmetic, so wide tensors show the hoist +/// while a narrow one is dominated by per-row framework costs. +const WIDTHS: &[usize] = &[2, 32, 256]; + +/// `ELEMENTS / width` vectors of `width` `f64` elements, non-nullable. `seed` offsets the values so +/// the two sides of the column arm are not the same array. +fn vectors(width: usize, seed: usize) -> ArrayRef { + let elements: Buffer = (0..ELEMENTS) + .map(|i| (((i + seed) % 97) as f64) - 48.0) + .collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ELEMENTS / width, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +/// One query vector of `width` `f64` elements broadcast to every row, as a [`ConstantArray`] over a +/// [`Vector`] extension scalar. +fn constant_vector(width: usize) -> ArrayRef { + let element_dtype = DType::Primitive(PType::F64, Nullability::NonNullable); + let children: Vec = (0..width) + .map(|i| Scalar::primitive(((i % 97) as f64) - 48.0, Nullability::NonNullable)) + .collect(); + let fsl_scalar = Scalar::fixed_size_list(element_dtype, children, Nullability::NonNullable); + let ext_scalar = Scalar::extension::(EmptyMetadata, fsl_scalar); + ConstantArray::new(ext_scalar, ELEMENTS / width).into_array() +} + +fn bench_cosine(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { + let session = vortex_array::array_session(); + bencher + .with_inputs(|| { + ( + CosineSimilarity::try_new_array(lhs.clone(), rhs.clone()) + .unwrap() + .into_array(), + session.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +/// The control: both operands vary by row, so every norm must be computed in the row loop. +#[divan::bench(args = WIDTHS)] +fn column_x_column(bencher: Bencher, width: usize) { + bench_cosine(bencher, vectors(width, 0), vectors(width, 31)); +} + +/// The rhs is a broadcast query vector, whose norm is the same in every row. +#[divan::bench(args = WIDTHS)] +fn column_x_constant(bencher: Bencher, width: usize) { + bench_cosine(bencher, vectors(width, 0), constant_vector(width)); +} + +/// One query vector represented as an extension array over constant storage. +fn extension_constant_vector(width: usize) -> ArrayRef { + let ext_dtype = vectors(width, 0).dtype().as_extension().clone(); + let element_dtype = DType::Primitive(PType::F64, Nullability::NonNullable); + let children: Vec = (0..width) + .map(|i| Scalar::primitive(((i % 97) as f64) - 48.0, Nullability::NonNullable)) + .collect(); + let fsl_scalar = Scalar::fixed_size_list(element_dtype, children, Nullability::NonNullable); + ExtensionArray::new( + ext_dtype, + ConstantArray::new(fsl_scalar, ELEMENTS / width).into_array(), + ) + .into_array() +} + +/// The rhs is the same broadcast query represented as extension-wrapped constant storage. +#[divan::bench(args = WIDTHS)] +fn column_x_extension_constant(bencher: Bencher, width: usize) { + bench_cosine(bencher, vectors(width, 0), extension_constant_vector(width)); +} diff --git a/vortex-tensor/benches/inner_product.rs b/vortex-tensor/benches/inner_product.rs new file mode 100644 index 00000000000..9e7a43abe5c --- /dev/null +++ b/vortex-tensor/benches/inner_product.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Baseline throughput for `inner_product` over tensor columns. +//! +//! The arms vary vector width and input nullability. Their names are intended to remain stable +//! across scalar-function implementation changes so CodSpeed can compare them against `develop`. +//! +//! Rows are derived from a fixed element budget rather than fixed per arm, so widening a vector +//! trades rows for elements instead of multiplying the work. See [`ELEMENTS`]. + +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::FixedSizeListArray; +use vortex_array::arrays::MaskedArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_tensor::scalar_fns::inner_product::InnerProduct; +use vortex_tensor::vector::Vector; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn main() { + divan::main(); +} + +/// Total `f64` elements per operand, held constant across widths: the row count is +/// `ELEMENTS / width`. CodSpeed's CPU simulation charges memory traffic far more than a desktop +/// does, so this budget is what keeps every arm inside the 1 ms per-iteration limit from +/// `docs/developer-guide/benchmarking.md`. +const ELEMENTS: usize = 16_384; + +const WIDTHS: &[usize] = &[2, 32, 256]; + +fn vectors(width: usize, seed: usize) -> ArrayRef { + let elements: Buffer = (0..ELEMENTS) + .map(|i| (((i + seed) % 97) as f64) - 48.0) + .collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ELEMENTS / width, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +fn bench_inner_product(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { + let session = vortex_array::array_session(); + bencher + .counter(ItemsCount::new(lhs.len())) + .with_inputs(|| { + ( + InnerProduct::try_new_array(lhs.clone(), rhs.clone()) + .unwrap() + .into_array(), + session.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +#[divan::bench(args = WIDTHS)] +fn non_nullable(bencher: Bencher, width: usize) { + bench_inner_product(bencher, vectors(width, 0), vectors(width, 31)); +} + +#[divan::bench(args = WIDTHS)] +fn nullable(bencher: Bencher, width: usize) { + let validity = Validity::from_iter((0..ELEMENTS / width).map(|i| i % 8 != 0)); + let lhs = MaskedArray::try_new(vectors(width, 0), validity) + .unwrap() + .into_array(); + bench_inner_product(bencher, lhs, vectors(width, 31)); +} diff --git a/vortex-tensor/benches/l2_norm.rs b/vortex-tensor/benches/l2_norm.rs new file mode 100644 index 00000000000..6f597084113 --- /dev/null +++ b/vortex-tensor/benches/l2_norm.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Baseline throughput for `l2_norm` over tensor columns. +//! +//! The arms vary vector width and input nullability. Their names are intended to remain stable +//! across scalar-function implementation changes so CodSpeed can compare them against `develop`. +//! +//! Rows are derived from a fixed element budget rather than fixed per arm, so widening a vector +//! trades rows for elements instead of multiplying the work. See [`ELEMENTS`]. + +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::FixedSizeListArray; +use vortex_array::arrays::MaskedArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_tensor::scalar_fns::l2_norm::L2Norm; +use vortex_tensor::vector::Vector; + +// Scalar function execution allocates its output inside the timed region, so use the vendored +// allocator instead of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn main() { + divan::main(); +} + +/// Total `f64` elements per operand, held constant across widths: the row count is +/// `ELEMENTS / width`. CodSpeed's CPU simulation charges memory traffic far more than a desktop +/// does, so this budget is what keeps every arm inside the 1 ms per-iteration limit from +/// `docs/developer-guide/benchmarking.md`. +const ELEMENTS: usize = 16_384; + +const WIDTHS: &[usize] = &[2, 32, 256]; + +fn vectors(width: usize) -> ArrayRef { + let elements: Buffer = (0..ELEMENTS).map(|i| ((i % 97) as f64) - 48.0).collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ELEMENTS / width, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +fn bench_l2_norm(bencher: Bencher, input: ArrayRef) { + let session = vortex_array::array_session(); + bencher + .counter(ItemsCount::new(input.len())) + .with_inputs(|| { + ( + L2Norm::try_new_array(input.clone()).unwrap().into_array(), + session.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +#[divan::bench(args = WIDTHS)] +fn non_nullable(bencher: Bencher, width: usize) { + bench_l2_norm(bencher, vectors(width)); +} + +#[divan::bench(args = WIDTHS)] +fn nullable(bencher: Bencher, width: usize) { + let validity = Validity::from_iter((0..ELEMENTS / width).map(|i| i % 8 != 0)); + let input = MaskedArray::try_new(vectors(width), validity) + .unwrap() + .into_array(); + bench_l2_norm(bencher, input); +} diff --git a/vortex-tensor/benches/normalized.rs b/vortex-tensor/benches/normalized.rs new file mode 100644 index 00000000000..29465333e58 --- /dev/null +++ b/vortex-tensor/benches/normalized.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Baseline throughput for decoding the `Normalized` encoding over tensor columns. +//! +//! The arms vary vector width and input nullability. Their names are intended to remain stable +//! across implementation changes so CodSpeed can compare them against `develop`. +//! +//! Rows are derived from a fixed element budget rather than fixed per arm, so widening a vector +//! trades rows for elements instead of multiplying the work. See [`ELEMENTS`]. + +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use divan::counter::ItemsCount; +use mimalloc::MiMalloc; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::ExtensionArray; +use vortex_array::arrays::FixedSizeListArray; +use vortex_array::arrays::MaskedArray; +use vortex_array::arrays::PrimitiveArray; +use vortex_array::validity::Validity; +use vortex_buffer::Buffer; +use vortex_tensor::encodings::normalized::Normalized; +use vortex_tensor::vector::Vector; + +// Decoding allocates the output inside the timed region, so use the vendored allocator instead +// of measuring glibc differences between CodSpeed runner images. +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn main() { + divan::main(); +} + +/// Total `f64` elements per operand, held constant across widths: the row count is +/// `ELEMENTS / width`. CodSpeed's CPU simulation charges memory traffic far more than a desktop +/// does, so this budget is what keeps every arm inside the 1 ms per-iteration limit from +/// `docs/developer-guide/benchmarking.md`. +const ELEMENTS: usize = 16_384; + +const WIDTHS: &[usize] = &[2, 32, 256]; + +fn normalized_vectors(width: usize) -> ArrayRef { + let value = 1.0 / (width as f64).sqrt(); + let elements: Buffer = (0..ELEMENTS).map(|_| value).collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ELEMENTS / width, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +fn norms(rows: usize) -> ArrayRef { + let values: Buffer = (0..rows).map(|i| 1.0 + ((i % 13) as f64) / 13.0).collect(); + PrimitiveArray::new(values, Validity::NonNullable).into_array() +} + +fn bench_normalized(bencher: Bencher, normalized: ArrayRef) { + let session = vortex_array::array_session(); + let rows = normalized.len(); + let norms = norms(rows); + bencher + .counter(ItemsCount::new(rows)) + .with_inputs(|| { + let mut ctx = session.create_execution_ctx(); + let array = Normalized::try_new(normalized.clone(), norms.clone(), &mut ctx).unwrap(); + (array, ctx) + }) + .bench_values(|(array, mut ctx)| { + array + .into_array() + .execute::(&mut ctx) + .unwrap() + }); +} + +#[divan::bench(args = WIDTHS)] +fn non_nullable(bencher: Bencher, width: usize) { + bench_normalized(bencher, normalized_vectors(width)); +} + +#[divan::bench(args = WIDTHS)] +fn nullable(bencher: Bencher, width: usize) { + let validity = Validity::from_iter((0..ELEMENTS / width).map(|i| i % 8 != 0)); + let normalized = MaskedArray::try_new(normalized_vectors(width), validity) + .unwrap() + .into_array(); + bench_normalized(bencher, normalized); +} From e95f4bc5569dc8718bef0d258cd5906b7fce4310 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 5 Aug 2026 11:59:42 -0400 Subject: [PATCH 2/3] Trim scalar function benchmarks Signed-off-by: Connor Tsui --- vortex-array/Cargo.toml | 8 - vortex-array/benches/binary_ops.rs | 2 +- vortex-array/benches/byte_length.rs | 98 ------------- vortex-array/benches/like.rs | 163 --------------------- vortex-geo/benches/binary_predicates.rs | 4 +- vortex-geo/benches/distance.rs | 4 +- vortex-geo/benches/envelope.rs | 7 +- vortex-tensor/benches/cosine_similarity.rs | 2 +- vortex-tensor/benches/inner_product.rs | 2 +- vortex-tensor/benches/normalized.rs | 2 +- 10 files changed, 10 insertions(+), 282 deletions(-) delete mode 100644 vortex-array/benches/byte_length.rs delete mode 100644 vortex-array/benches/like.rs diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index 4bcab93198a..4c590e924fd 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -133,14 +133,6 @@ harness = false name = "kleene_bool" harness = false -[[bench]] -name = "like" -harness = false - -[[bench]] -name = "byte_length" -harness = false - [[bench]] name = "interleave" harness = false diff --git a/vortex-array/benches/binary_ops.rs b/vortex-array/benches/binary_ops.rs index 00ad82d3457..d83d172e0a5 100644 --- a/vortex-array/benches/binary_ops.rs +++ b/vortex-array/benches/binary_ops.rs @@ -36,7 +36,7 @@ fn main() { static SESSION: LazyLock = LazyLock::new(array_session); -const LEN: usize = 65_536; +const LEN: usize = 32_768; #[divan::bench] fn add_i64_nonnull(bencher: Bencher) { diff --git a/vortex-array/benches/byte_length.rs b/vortex-array/benches/byte_length.rs deleted file mode 100644 index e64cb98b562..00000000000 --- a/vortex-array/benches/byte_length.rs +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -//! Baseline throughput for `byte_length` over UTF-8 view arrays. -//! -//! The arms cover inline and out-of-line views, plus nullable out-of-line views. Their names are -//! intended to remain stable across scalar-function implementation changes so CodSpeed can compare -//! them against `develop`. - -#![expect(clippy::unwrap_used)] - -use std::sync::LazyLock; - -use divan::Bencher; -use divan::counter::ItemsCount; -use mimalloc::MiMalloc; -use vortex_array::ArrayRef; -use vortex_array::Canonical; -use vortex_array::IntoArray; -use vortex_array::VortexSessionExecute; -use vortex_array::array_session; -use vortex_array::arrays::MaskedArray; -use vortex_array::arrays::ScalarFnArray; -use vortex_array::arrays::VarBinViewArray; -use vortex_array::scalar_fn::EmptyOptions; -use vortex_array::scalar_fn::TypedScalarFnInstance; -use vortex_array::scalar_fn::fns::byte_length::ByteLength; -use vortex_array::validity::Validity; -use vortex_session::VortexSession; - -// Scalar function execution allocates its output inside the timed region, so use the vendored -// allocator instead of measuring glibc differences between CodSpeed runner images. -#[global_allocator] -static GLOBAL: MiMalloc = MiMalloc; - -static SESSION: LazyLock = LazyLock::new(array_session); - -fn main() { - LazyLock::force(&SESSION); - divan::main(); -} - -const SIZES: &[usize] = &[4_096, 65_536]; - -fn long_strings(len: usize) -> ArrayRef { - VarBinViewArray::from_iter_str((0..len).map(|i| format!("a string well past inline: {i}"))) - .into_array() -} - -fn short_strings(len: usize) -> ArrayRef { - VarBinViewArray::from_iter_str((0..len).map(|i| format!("{}", i % 1_000))).into_array() -} - -fn bench_byte_length(bencher: Bencher, input: ArrayRef) { - let len = input.len(); - bencher - .counter(ItemsCount::new(len)) - .with_inputs(|| { - ( - ScalarFnArray::try_new( - TypedScalarFnInstance::new(ByteLength, EmptyOptions).erased(), - vec![input.clone()], - ) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), - ) - }) - .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); -} - -#[divan::bench(args = SIZES)] -fn inline(bencher: Bencher, len: usize) { - bench_byte_length(bencher, short_strings(len)); -} - -#[divan::bench(args = SIZES)] -fn out_of_line(bencher: Bencher, len: usize) { - bench_byte_length(bencher, long_strings(len)); -} - -#[divan::bench(args = SIZES)] -fn nullable_out_of_line(bencher: Bencher, len: usize) { - let validity = Validity::from_iter((0..len).map(|i| i % 8 != 0)); - let input = MaskedArray::try_new(long_strings(len), validity) - .unwrap() - .into_array(); - bench_byte_length(bencher, input); -} - -#[divan::bench(args = SIZES)] -fn nullable_out_of_line_90pct(bencher: Bencher, len: usize) { - let validity = Validity::from_iter((0..len).map(|i| i.is_multiple_of(10))); - let input = MaskedArray::try_new(long_strings(len), validity) - .unwrap() - .into_array(); - bench_byte_length(bencher, input); -} diff --git a/vortex-array/benches/like.rs b/vortex-array/benches/like.rs deleted file mode 100644 index 1dc1f2c240b..00000000000 --- a/vortex-array/benches/like.rs +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -#![expect(clippy::unwrap_used)] - -use divan::Bencher; -use rand::RngExt; -use rand::SeedableRng; -use rand::distr::Uniform; -use rand::prelude::StdRng; -use vortex_array::ArrayRef; -use vortex_array::IntoArray; -use vortex_array::VortexSessionExecute; -use vortex_array::arrays::BoolArray; -use vortex_array::arrays::ConstantArray; -use vortex_array::arrays::ScalarFnArray; -use vortex_array::arrays::VarBinViewArray; -use vortex_array::scalar_fn::TypedScalarFnInstance; -use vortex_array::scalar_fn::fns::like::Like; -use vortex_array::scalar_fn::fns::like::LikeOptions; - -fn main() { - divan::main(); -} - -const ARRAY_SIZE: usize = 2_048; - -/// The per-row pattern arms compile one pattern per row in the worst case, which costs an order of -/// magnitude more per row than matching against a constant pattern. They use a smaller fixture to -/// stay inside the 1 ms per-iteration budget from `docs/developer-guide/benchmarking.md`. -const PER_ROW_PATTERN_ROWS: usize = 512; - -/// Random lowercase strings of 4..=24 bytes, some with a `hello` infix. -fn strings(len: usize) -> ArrayRef { - let mut rng = StdRng::seed_from_u64(0); - let len_dist = Uniform::new_inclusive(4usize, 24).unwrap(); - VarBinViewArray::from_iter_str((0..len).map(|i| { - let len = rng.sample(len_dist); - let mut s: String = (0..len) - .map(|_| char::from(rng.random_range(b'a'..=b'z'))) - .collect(); - if i % 7 == 0 { - s.insert_str(len / 2, "hello"); - } - s - })) - .into_array() -} - -fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) { - let session = vortex_array::array_session(); - let array = strings(ARRAY_SIZE); - bencher - .with_inputs(|| { - ( - ScalarFnArray::try_new( - TypedScalarFnInstance::new(Like, options).erased(), - vec![ - array.clone(), - ConstantArray::new(pattern, ARRAY_SIZE).into_array(), - ], - ) - .unwrap() - .into_array(), - session.create_execution_ctx(), - ) - }) - .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); -} - -#[divan::bench] -fn like_exact(bencher: Bencher) { - bench_like(bencher, "hello", LikeOptions::default()); -} - -#[divan::bench] -fn like_prefix(bencher: Bencher) { - bench_like(bencher, "hello%", LikeOptions::default()); -} - -#[divan::bench] -fn like_suffix(bencher: Bencher) { - bench_like(bencher, "%hello", LikeOptions::default()); -} - -#[divan::bench] -fn like_contains(bencher: Bencher) { - bench_like(bencher, "%hello%", LikeOptions::default()); -} - -#[divan::bench] -fn like_regex(bencher: Bencher) { - bench_like(bencher, "h_llo%w%d", LikeOptions::default()); -} - -fn bench_per_row_patterns(bencher: Bencher, patterns: ArrayRef) { - let session = vortex_array::array_session(); - let array = strings(patterns.len()); - bencher - .with_inputs(|| { - ( - ScalarFnArray::try_new( - TypedScalarFnInstance::new(Like, LikeOptions::default()).erased(), - vec![array.clone(), patterns.clone()], - ) - .unwrap() - .into_array(), - session.create_execution_ctx(), - ) - }) - .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); -} - -#[divan::bench] -fn like_per_row_patterns(bencher: Bencher) { - // A non-constant pattern child takes the per-row path; repeated patterns hit the compile cache. - let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array(); - bench_per_row_patterns(bencher, patterns); -} - -/// The cached half of the compile-cache pair: every row repeats one pattern whose five-byte shape -/// matches the distinct-pattern arm, so the two differ only in whether the cache hits. Both use -/// [`PER_ROW_PATTERN_ROWS`] rows to stay comparable. -/// -/// [`like_per_row_distinct_patterns`] -#[divan::bench] -fn like_per_row_repeated_patterns(bencher: Bencher) { - let patterns = - VarBinViewArray::from_iter_str((0..PER_ROW_PATTERN_ROWS).map(|_| "%aaa%")).into_array(); - bench_per_row_patterns(bencher, patterns); -} - -/// The per-row path with the compile cache defeated: every row carries a distinct pattern of the -/// same shape as the repeated-pattern arm, so each row pays one pattern compilation. -/// -/// [`like_per_row_repeated_patterns`] -#[divan::bench] -fn like_per_row_distinct_patterns(bencher: Bencher) { - let patterns = VarBinViewArray::from_iter_str( - (0..PER_ROW_PATTERN_ROWS).map(|i| format!("%{}%", distinct_trigram(i))), - ) - .into_array(); - bench_per_row_patterns(bencher, patterns); -} - -/// A distinct three-letter lowercase infix per row, so every pattern has the same shape while -/// no two rows share a compiled pattern. -fn distinct_trigram(i: usize) -> String { - let letter = |shift: usize| char::from(b'a' + u8::try_from((i >> shift) % 26).unwrap()); - [letter(0), letter(5), letter(10)].iter().collect() -} - -#[divan::bench] -fn ilike_contains(bencher: Bencher) { - bench_like( - bencher, - "%HELLO%", - LikeOptions { - negated: false, - case_insensitive: true, - }, - ); -} diff --git a/vortex-geo/benches/binary_predicates.rs b/vortex-geo/benches/binary_predicates.rs index af934724a4a..e8aeff53b10 100644 --- a/vortex-geo/benches/binary_predicates.rs +++ b/vortex-geo/benches/binary_predicates.rs @@ -58,11 +58,11 @@ static SESSION: LazyLock = LazyLock::new(geo_session); /// predicate costs roughly a microsecond per row under CodSpeed's CPU simulation, which caps the /// row count well below a typical batch to stay inside the 1 ms per-iteration budget from /// `docs/developer-guide/benchmarking.md`. -const ROWS: usize = 1 << 8; +const ROWS: usize = 1 << 7; /// The all-overlapping polygon arm never rejects on bounding boxes, so every row pays for the full /// pairwise predicate. It needs a smaller fixture than [`ROWS`] to stay inside the same budget. -const OVERLAPPING_POLYGON_ROWS: usize = 1 << 6; +const OVERLAPPING_POLYGON_ROWS: usize = 1 << 5; /// Deterministic pseudo-random value in `[0, 1)`. fn unit(i: usize) -> f64 { diff --git a/vortex-geo/benches/distance.rs b/vortex-geo/benches/distance.rs index 96e143359de..3abd8d91d05 100644 --- a/vortex-geo/benches/distance.rs +++ b/vortex-geo/benches/distance.rs @@ -37,8 +37,8 @@ static GLOBAL: MiMalloc = MiMalloc; static SESSION: LazyLock = LazyLock::new(geo_session); -const POINT_ROWS: usize = 1_024; -const POLYGON_ROWS: usize = 64; +const POINT_ROWS: usize = 512; +const POLYGON_ROWS: usize = 32; fn main() { divan::main(); diff --git a/vortex-geo/benches/envelope.rs b/vortex-geo/benches/envelope.rs index f15516437b5..1aa26d86207 100644 --- a/vortex-geo/benches/envelope.rs +++ b/vortex-geo/benches/envelope.rs @@ -48,11 +48,8 @@ fn main() { static SESSION: LazyLock = LazyLock::new(geo_session); /// Every case has the same row count so results are comparable across shapes: differences then -/// reflect per-row cost (nesting depth, validity handling) rather than input size. The nested cases -/// carry 32 vertices per row, and CodSpeed's CPU simulation charges their memory traffic far more -/// than a desktop does, so this is what keeps them inside the 1 ms per-iteration budget from -/// `docs/developer-guide/benchmarking.md`. -const ROWS: usize = 1 << 11; +/// reflect per-row cost (nesting depth, validity handling) rather than input size. +const ROWS: usize = 1 << 9; /// Deterministic pseudo-random ordinate in `[0, 1000)`. fn ordinate(i: usize) -> f64 { diff --git a/vortex-tensor/benches/cosine_similarity.rs b/vortex-tensor/benches/cosine_similarity.rs index feb860f3372..6cc5eb867ef 100644 --- a/vortex-tensor/benches/cosine_similarity.rs +++ b/vortex-tensor/benches/cosine_similarity.rs @@ -45,7 +45,7 @@ fn main() { /// the constant arms recompute the broadcast vector's norm per row and cost roughly ten times the /// column arms per element. It is what keeps every arm inside the 1 ms per-iteration limit from /// `docs/developer-guide/benchmarking.md`, measured against CodSpeed's CPU simulation. -const ELEMENTS: usize = 4_096; +const ELEMENTS: usize = 2_048; /// Widths chosen to separate the two costs, as in `l2_norm.rs`: the redundant norm pass is /// `O(rows * width)`, one third of the closure's arithmetic, so wide tensors show the hoist diff --git a/vortex-tensor/benches/inner_product.rs b/vortex-tensor/benches/inner_product.rs index 9e7a43abe5c..796e9b648d6 100644 --- a/vortex-tensor/benches/inner_product.rs +++ b/vortex-tensor/benches/inner_product.rs @@ -38,7 +38,7 @@ fn main() { /// `ELEMENTS / width`. CodSpeed's CPU simulation charges memory traffic far more than a desktop /// does, so this budget is what keeps every arm inside the 1 ms per-iteration limit from /// `docs/developer-guide/benchmarking.md`. -const ELEMENTS: usize = 16_384; +const ELEMENTS: usize = 8_192; const WIDTHS: &[usize] = &[2, 32, 256]; diff --git a/vortex-tensor/benches/normalized.rs b/vortex-tensor/benches/normalized.rs index 29465333e58..39ef3396866 100644 --- a/vortex-tensor/benches/normalized.rs +++ b/vortex-tensor/benches/normalized.rs @@ -39,7 +39,7 @@ fn main() { /// `ELEMENTS / width`. CodSpeed's CPU simulation charges memory traffic far more than a desktop /// does, so this budget is what keeps every arm inside the 1 ms per-iteration limit from /// `docs/developer-guide/benchmarking.md`. -const ELEMENTS: usize = 16_384; +const ELEMENTS: usize = 8_192; const WIDTHS: &[usize] = &[2, 32, 256]; From ca01627729752341b4e11fe02027a3e28b349b7d Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 5 Aug 2026 12:07:53 -0400 Subject: [PATCH 3/3] Restore LIKE benchmark Signed-off-by: Connor Tsui --- vortex-array/Cargo.toml | 4 ++ vortex-array/benches/like.rs | 122 +++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 vortex-array/benches/like.rs diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index 4c590e924fd..70fba4acdcf 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -133,6 +133,10 @@ harness = false name = "kleene_bool" harness = false +[[bench]] +name = "like" +harness = false + [[bench]] name = "interleave" harness = false diff --git a/vortex-array/benches/like.rs b/vortex-array/benches/like.rs new file mode 100644 index 00000000000..68219724717 --- /dev/null +++ b/vortex-array/benches/like.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![expect(clippy::unwrap_used)] + +use divan::Bencher; +use rand::RngExt; +use rand::SeedableRng; +use rand::distr::Uniform; +use rand::prelude::StdRng; +use vortex_array::ArrayRef; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::arrays::BoolArray; +use vortex_array::arrays::ConstantArray; +use vortex_array::arrays::VarBinViewArray; +use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; +use vortex_array::scalar_fn::fns::like::Like; +use vortex_array::scalar_fn::fns::like::LikeOptions; + +fn main() { + divan::main(); +} + +const ARRAY_SIZE: usize = 2_048; + +/// Random lowercase strings of 4..=24 bytes, some with a `hello` infix. +fn strings() -> ArrayRef { + let mut rng = StdRng::seed_from_u64(0); + let len_dist = Uniform::new_inclusive(4usize, 24).unwrap(); + VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|i| { + let len = rng.sample(len_dist); + let mut s: String = (0..len) + .map(|_| char::from(rng.random_range(b'a'..=b'z'))) + .collect(); + if i % 7 == 0 { + s.insert_str(len / 2, "hello"); + } + s + })) + .into_array() +} + +fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) { + let session = vortex_array::array_session(); + let array = strings(); + bencher + .with_inputs(|| { + ( + Like.try_new_array( + ARRAY_SIZE, + options, + [ + array.clone(), + ConstantArray::new(pattern, ARRAY_SIZE).into_array(), + ], + ) + .unwrap(), + session.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +#[divan::bench] +fn like_exact(bencher: Bencher) { + bench_like(bencher, "hello", LikeOptions::default()); +} + +#[divan::bench] +fn like_prefix(bencher: Bencher) { + bench_like(bencher, "hello%", LikeOptions::default()); +} + +#[divan::bench] +fn like_suffix(bencher: Bencher) { + bench_like(bencher, "%hello", LikeOptions::default()); +} + +#[divan::bench] +fn like_contains(bencher: Bencher) { + bench_like(bencher, "%hello%", LikeOptions::default()); +} + +#[divan::bench] +fn like_regex(bencher: Bencher) { + bench_like(bencher, "h_llo%w%d", LikeOptions::default()); +} + +#[divan::bench] +fn like_per_row_patterns(bencher: Bencher) { + let session = vortex_array::array_session(); + let array = strings(); + // A non-constant pattern child takes the per-row path; repeated patterns hit the + // compile cache. + let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array(); + bencher + .with_inputs(|| { + ( + Like.try_new_array( + ARRAY_SIZE, + LikeOptions::default(), + [array.clone(), patterns.clone()], + ) + .unwrap(), + session.create_execution_ctx(), + ) + }) + .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); +} + +#[divan::bench] +fn ilike_contains(bencher: Bencher) { + bench_like( + bencher, + "%HELLO%", + LikeOptions { + negated: false, + case_insensitive: true, + }, + ); +}