From 9688ad0328854778f8ef451a19c1a64f727f9e5a Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 24 Aug 2026 21:37:02 +0000 Subject: [PATCH] Measure numeric arithmetic and comparison benchmarks per CPU feature The binary arithmetic and comparison kernels are portable lane loops: the source is the same on every target and the vector width the compiler picks comes from the build flags. That is exactly what `#[cpu_features]` is for, so the microbenchmarks over them move from simulation onto the walltime legs, where they are measured under each feature set on silicon that implements it. Tagged: the primitive arithmetic and comparison cases in `binary_ops`, the integer and float cases in `compare`, `scalar_subtract`, and the checked-add pair in `lane_kernels` -- the arrow-rs baseline included, since comparing the two is only meaningful under the same build flags. Left in simulation: decimal arithmetic (`i128` widening and per-lane rescaling), the boolean kernels (already word-at-a-time over a bitmap), and the string and struct comparisons (view chasing and per-field dispatch). A wider vector register is not what decides any of them. Signed-off-by: Connor Tsui --- Cargo.lock | 1 + vortex-array/benches/binary_ops.rs | 34 +++++++++++++++++++++++++ vortex-array/benches/compare.rs | 22 +++++++++++----- vortex-array/benches/scalar_subtract.rs | 1 + vortex-compute/Cargo.toml | 1 + vortex-compute/benches/lane_kernels.rs | 7 +++++ 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2508ba7a3e8..43862759c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9892,6 +9892,7 @@ dependencies = [ "codspeed-divan-compat", "num-traits", "rand 0.10.2", + "vortex-bench-support", "vortex-buffer", ] diff --git a/vortex-array/benches/binary_ops.rs b/vortex-array/benches/binary_ops.rs index b677d50d681..da0b8bd2430 100644 --- a/vortex-array/benches/binary_ops.rs +++ b/vortex-array/benches/binary_ops.rs @@ -1,6 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +//! Benchmarks for the binary `Operator` path, over primitive, decimal, and boolean inputs. +//! +//! The primitive arithmetic and comparison cases carry `#[cpu_features]`, so they are +//! measured on every walltime CPU-feature leg rather than in simulation. Each is written +//! once and compiled differently per leg: the kernel underneath is a portable lane loop, and +//! how wide the compiler vectorizes it is decided by the build flags, not by the source. +//! +//! The decimal and boolean cases are not tagged. Decimal arithmetic is `i128` widening and +//! per-lane rescaling, and the boolean kernels are already word-at-a-time over a bitmap; +//! neither is where a wider vector register shows up. They stay in simulation. + #![expect(clippy::unwrap_used)] #![expect( clippy::cast_possible_truncation, @@ -73,16 +84,19 @@ enum BinaryShape { /// the instrumented CodSpeed runs quick. const DECIMAL_MUL_DIV_LEN: usize = 1_024; +#[vortex_bench_support::cpu_features] #[divan::bench(args = BINARY_SHAPE_CASES)] fn add_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) { bench_binary_shape(bencher, len, shape, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench(args = BINARY_SHAPE_CASES)] fn subtract_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) { bench_binary_shape(bencher, len, shape, Operator::Sub); } +#[vortex_bench_support::cpu_features] #[divan::bench(args = BINARY_SHAPE_CASES)] fn multiply_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) { bench_binary_shape(bencher, len, shape, Operator::Mul); @@ -104,6 +118,7 @@ fn bench_binary_shape(bencher: Bencher, len: usize, shape: BinaryShape, operator bench_primitive(bencher, lhs, rhs, operator); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn add_i64_nonnull(bencher: Bencher) { let lhs = primitive_nonnull(0, I64_LEN).into_array(); @@ -112,6 +127,7 @@ fn add_i64_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn add_i64_nullable(bencher: Bencher) { let lhs = primitive_nullable(0, 7, I64_LEN).into_array(); @@ -120,6 +136,7 @@ fn add_i64_nullable(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn add_i64_constant(bencher: Bencher) { let lhs = primitive_nonnull(0, I64_LEN).into_array(); @@ -128,6 +145,7 @@ fn add_i64_constant(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn add_i32_nonnull(bencher: Bencher) { let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array(); @@ -136,6 +154,7 @@ fn add_i32_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn add_u32_nonnull(bencher: Bencher) { let lhs = primitive_u32_small_nonnull(1, I32_LEN).into_array(); @@ -144,6 +163,7 @@ fn add_u32_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Add); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i64_nonnull(bencher: Bencher) { let lhs = primitive_small_nonnull(1, I64_LEN).into_array(); @@ -152,6 +172,7 @@ fn mul_i64_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i8_nonnull(bencher: Bencher) { let lhs = primitive_i8_small_nonnull(1, I8_LEN).into_array(); @@ -160,6 +181,7 @@ fn mul_i8_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_u8_nonnull(bencher: Bencher) { let lhs = primitive_u8_small_nonnull(1, I8_LEN).into_array(); @@ -168,6 +190,7 @@ fn mul_u8_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i16_nonnull(bencher: Bencher) { let lhs = primitive_i16_small_nonnull(1, I16_LEN).into_array(); @@ -176,6 +199,7 @@ fn mul_i16_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_u16_nonnull(bencher: Bencher) { let lhs = primitive_u16_small_nonnull(1, I16_LEN).into_array(); @@ -184,6 +208,7 @@ fn mul_u16_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i32_nonnull(bencher: Bencher) { let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array(); @@ -192,6 +217,7 @@ fn mul_i32_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_u32_nonnull(bencher: Bencher) { let lhs = primitive_u32_small_nonnull(1, I32_LEN).into_array(); @@ -200,6 +226,7 @@ fn mul_u32_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_u64_nonnull(bencher: Bencher) { let lhs = primitive_u64_small_nonnull(1, I64_LEN).into_array(); @@ -208,6 +235,7 @@ fn mul_u64_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i32_nullable(bencher: Bencher) { let lhs = primitive_i32_small_nullable(1, 7, I32_LEN).into_array(); @@ -216,6 +244,7 @@ fn mul_i32_nullable(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn mul_i32_constant(bencher: Bencher) { let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array(); @@ -224,6 +253,7 @@ fn mul_i32_constant(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Mul); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn div_i64_nonnull(bencher: Bencher) { let lhs = primitive_nonnull(1_000_000, I64_LEN).into_array(); @@ -232,6 +262,7 @@ fn div_i64_nonnull(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Div); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn div_i64_nullable(bencher: Bencher) { let lhs = primitive_nullable(1_000_000, 7, I64_LEN).into_array(); @@ -240,6 +271,7 @@ fn div_i64_nullable(bencher: Bencher) { bench_primitive(bencher, lhs, rhs, Operator::Div); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn sub_i64_constant(bencher: Bencher) { let lhs = primitive_nonnull(0, I64_LEN).into_array(); @@ -296,6 +328,7 @@ fn div_decimal_i128_nullable(bencher: Bencher) { bench_decimal(bencher, lhs, rhs, Operator::Div); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn eq_i64_constant(bencher: Bencher) { let lhs = primitive_nonnull(0, LEN).into_array(); @@ -304,6 +337,7 @@ fn eq_i64_constant(bencher: Bencher) { bench_bool(bencher, lhs, rhs, Operator::Eq); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn lt_i64_nullable(bencher: Bencher) { let lhs = primitive_nullable(0, 7, LEN).into_array(); diff --git a/vortex-array/benches/compare.rs b/vortex-array/benches/compare.rs index 2a0aec03885..59f38c20230 100644 --- a/vortex-array/benches/compare.rs +++ b/vortex-array/benches/compare.rs @@ -3,12 +3,17 @@ //! Benchmarks for the binary comparison path, over every array kind it accepts. //! -//! `compare_int_constant_left`, `compare_u8`, `compare_u64`, and `compare_f32` carry -//! `#[cpu_features]`, so they are measured on every walltime CPU-feature leg rather than in -//! simulation. Each is written once and compiled differently per leg: today the primitive -//! comparison path is a portable lane kernel, and how well it auto-vectorizes is decided by -//! the build. That is the baseline a hand-written kernel selected through -//! `cfg(target_feature)` has to beat, measured on the silicon it would run on. +//! The primitive cases carry `#[cpu_features]`, so they are measured on every walltime +//! CPU-feature leg rather than in simulation. Each is written once and compiled differently +//! per leg: today the primitive comparison path is a portable lane kernel, and how well it +//! auto-vectorizes is decided by the build. That is the baseline a hand-written kernel +//! selected through `cfg(target_feature)` has to beat, measured on the silicon it would run +//! on. +//! +//! The boolean, decimal, string, and struct cases are not tagged. A wider vector register is +//! not what decides them: booleans are already word-at-a-time over a bitmap, decimals are +//! `i128`, and the string and struct cases are dominated by view chasing and per-field +//! dispatch. They stay in simulation. #![expect(clippy::unwrap_used)] @@ -164,6 +169,7 @@ fn compare_bool_constant(bencher: Bencher) { bench_compare(bencher, arr, constant, Operator::Eq); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn compare_int(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); @@ -172,6 +178,7 @@ fn compare_int(bencher: Bencher) { bench_compare(bencher, arr1, arr2, Operator::Gte); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn compare_int_nullable(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); @@ -180,6 +187,7 @@ fn compare_int_nullable(bencher: Bencher) { bench_compare(bencher, arr1, arr2, Operator::Gte); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn compare_int_constant(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); @@ -224,6 +232,7 @@ fn compare_f32(bencher: Bencher) { bench_compare(bencher, arr1, arr2, Operator::Gte); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn compare_int_eq(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); @@ -232,6 +241,7 @@ fn compare_int_eq(bencher: Bencher) { bench_compare(bencher, arr1, arr2, Operator::Eq); } +#[vortex_bench_support::cpu_features] #[divan::bench] fn compare_float(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); diff --git a/vortex-array/benches/scalar_subtract.rs b/vortex-array/benches/scalar_subtract.rs index 402f39bc0c9..50c0d903f20 100644 --- a/vortex-array/benches/scalar_subtract.rs +++ b/vortex-array/benches/scalar_subtract.rs @@ -29,6 +29,7 @@ fn main() { static SESSION: LazyLock = LazyLock::new(array_session); +#[vortex_bench_support::cpu_features] #[divan::bench] fn scalar_subtract(bencher: Bencher) { let mut rng = StdRng::seed_from_u64(0); diff --git a/vortex-compute/Cargo.toml b/vortex-compute/Cargo.toml index f223d455395..f8b710aacc5 100644 --- a/vortex-compute/Cargo.toml +++ b/vortex-compute/Cargo.toml @@ -28,6 +28,7 @@ arrow-schema = { workspace = true } divan = { workspace = true } num-traits = { workspace = true } rand = { workspace = true } +vortex-bench-support = { workspace = true } [lints] workspace = true diff --git a/vortex-compute/benches/lane_kernels.rs b/vortex-compute/benches/lane_kernels.rs index eca258ed555..2e9e145add7 100644 --- a/vortex-compute/benches/lane_kernels.rs +++ b/vortex-compute/benches/lane_kernels.rs @@ -12,6 +12,11 @@ //! Each Vortex kernel bench has a sibling `arrow_*` baseline bench using the //! equivalent arrow-rs kernel over the same data shape, so the divan report //! lines up side-by-side. +//! +//! The checked-add pair carries `#[cpu_features]`, so both are measured on every +//! walltime CPU-feature leg rather than in simulation: they are one lane loop +//! compiled differently per leg, and comparing them against arrow-rs is only +//! meaningful under the same build flags. The cast benches stay in simulation. #![expect(clippy::unwrap_used)] #![expect(clippy::clone_on_ref_ptr)] @@ -324,6 +329,7 @@ fn add_fixture(n: usize) -> AddFixture { } } +#[vortex_bench_support::cpu_features] #[divan::bench(args = SIZES)] fn lanezip_checked_add_u32(bencher: Bencher, n: usize) { let f = add_fixture(n); @@ -346,6 +352,7 @@ fn lanezip_checked_add_u32(bencher: Bencher, n: usize) { }); } +#[vortex_bench_support::cpu_features] #[divan::bench(args = SIZES)] fn arrow_checked_add_u32(bencher: Bencher, n: usize) { let f = add_fixture(n);