From 9755708049c137120e772ab3dd92c63ef5b4b58a Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sat, 1 Aug 2026 13:27:05 -0400 Subject: [PATCH] Add scalar function performance baselines Adds stable CodSpeed baselines for public construction and execution paths across byte length, binary arithmetic, LIKE, tensor scalar functions, and geo operations. The benches avoid RowFn test-harness internals so later implementation changes compare against the production path. `like_per_row_patterns` keeps its original repeated `hello%` fixture so its CodSpeed history stays comparable. The compile-cache pair lands under new names: `like_per_row_repeated_patterns` and `like_per_row_distinct_patterns` share a five-byte pattern shape and differ only in whether the cache hits. Geo distance covers point and polygon columns, including constants and nullable points. Geo predicate coverage adds sparse and mixed validity cases, while its all-overlapping polygon arm uses 1,024 rows because CodSpeed simulates the compiled amd64/AVX2 machine code. Each benchmark binary that allocates inside the timed region uses vendored `mimalloc`. `Normalized` keeps its checked inherent constructor because its normalized-row invariant requires construction-time validation. 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 | 66 +++- vortex-geo/Cargo.toml | 9 + vortex-geo/benches/binary_predicates.rs | 393 +++++++++++++++++++++ vortex-geo/benches/distance.rs | 143 ++++++++ vortex-geo/benches/envelope.rs | 6 + vortex-tensor/Cargo.toml | 18 + vortex-tensor/benches/cosine_similarity.rs | 116 ++++++ vortex-tensor/benches/inner_product.rs | 78 ++++ vortex-tensor/benches/l2_norm.rs | 76 ++++ vortex-tensor/benches/normalized.rs | 86 +++++ 15 files changed, 1105 insertions(+), 16 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 d27a977e11a..68d8f644e9d 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 db19fe686e9..65c8d93cc72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10073,6 +10073,7 @@ dependencies = [ "geo-types", "geoarrow", "geoarrow-cast", + "mimalloc", "prost 0.14.4", "rstest", "vortex-array", @@ -10486,8 +10487,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..321074b80df 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; @@ -47,15 +48,15 @@ fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) { 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 +88,61 @@ 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(); 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. +/// +/// [`like_per_row_distinct_patterns`] +#[divan::bench] +fn like_per_row_repeated_patterns(bencher: Bencher) { + let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).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..ARRAY_SIZE).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..1829a02b72e --- /dev/null +++ b/vortex-geo/benches/binary_predicates.rs @@ -0,0 +1,393 @@ +// 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`), split into a mostly-disjoint and a mostly-overlapping +//! dataset so the bbox early-out's win and its overhead are both visible. 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. +//! +//! 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. +const ROWS: usize = 1 << 14; + +/// The overlapping-polygon arm pays for topology graphs on every row. Keep its CodSpeed CPU +/// simulation workload bounded without shrinking the rest of the production baselines. +const OVERLAPPING_POLYGON_ROWS: usize = 1 << 10; + +/// 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 overlapping polygons. Each row reaches the full topology path, + /// so this arm uses a smaller fixture while the ordinary geo baselines remain at [`ROWS`]. + #[divan::bench] + fn constant_x_polygons_overlapping(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let query = query_constant(&mut ctx, OVERLAPPING_POLYGON_ROWS); + let polygons = squares_mostly_overlapping(OVERLAPPING_POLYGON_ROWS); + bencher + .counter(ItemsCount::new(OVERLAPPING_POLYGON_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. + #[divan::bench] + fn polygons_overlapping_x_constant(bencher: Bencher) { + let mut ctx = SESSION.create_execution_ctx(); + let polygons = squares_mostly_overlapping(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, + ) + }); + } + + /// 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..3f68514b3a5 --- /dev/null +++ b/vortex-geo/benches/distance.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Production baselines for planar `ST_Distance` over native geometry columns. +//! +//! Point cases use 16,384 rows. Polygon distance rebuilds both geometry R-trees per call, so its +//! smaller fixture still exercises that work without making CodSpeed's amd64/AVX2 CPU simulation +//! dominate the shard. + +#![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 = 16_384; +const POLYGON_ROWS: usize = 1_024; + +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..2115cf88c9c 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(); } 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..e44a3210da0 --- /dev/null +++ b/vortex-tensor/benches/cosine_similarity.rs @@ -0,0 +1,116 @@ +// 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`. + +#![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(); +} + +const ROWS: usize = 16_384; + +/// 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]; + +/// [`ROWS`] 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..ROWS * width) + .map(|i| (((i + seed) % 97) as f64) - 48.0) + .collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ROWS, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +/// One query vector of `width` `f64` elements broadcast to [`ROWS`] rows, 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, ROWS).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, ROWS).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..dbfb9e1612d --- /dev/null +++ b/vortex-tensor/benches/inner_product.rs @@ -0,0 +1,78 @@ +// 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`. + +#![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(); +} + +const ROWS: usize = 16_384; +const WIDTHS: &[usize] = &[2, 32, 256]; + +fn vectors(width: usize, seed: usize) -> ArrayRef { + let elements: Buffer = (0..ROWS * width) + .map(|i| (((i + seed) % 97) as f64) - 48.0) + .collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ROWS, + ) + .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(ROWS)) + .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..ROWS).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..bcc71fe8a34 --- /dev/null +++ b/vortex-tensor/benches/l2_norm.rs @@ -0,0 +1,76 @@ +// 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`. + +#![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(); +} + +const ROWS: usize = 16_384; +const WIDTHS: &[usize] = &[2, 32, 256]; + +fn vectors(width: usize) -> ArrayRef { + let elements: Buffer = (0..ROWS * width) + .map(|i| ((i % 97) as f64) - 48.0) + .collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ROWS, + ) + .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(ROWS)) + .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..ROWS).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..fd164f82d15 --- /dev/null +++ b/vortex-tensor/benches/normalized.rs @@ -0,0 +1,86 @@ +// 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`. + +#![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(); +} + +const ROWS: 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..ROWS * width).map(|_| value).collect(); + let storage = FixedSizeListArray::new( + elements.into_array(), + u32::try_from(width).unwrap(), + Validity::NonNullable, + ROWS, + ) + .into_array(); + Vector::try_new_vector_array(storage).unwrap() +} + +fn norms() -> 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 norms = norms(); + 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..ROWS).map(|i| i % 8 != 0)); + let normalized = MaskedArray::try_new(normalized_vectors(width), validity) + .unwrap() + .into_array(); + bench_normalized(bencher, normalized); +}