Skip to content

Commit 17bd9e2

Browse files
authored
Measure numeric arithmetic and comparison benchmarks per CPU feature (#9599)
## Summary Sweeps `#[cpu_features]` across the microbenchmarks that clearly earn it: the binary numeric arithmetic and comparison kernels. Those are portable lane loops — the source is identical on every target and the vector width the compiler picks comes from the build flags — which is the case the attribute exists for. Measuring them in simulation under one fixed `+avx2` build hides the only variable that matters. Middle of a three-PR stack: #9598, then this PR, then #9587. It carries no kernel changes of its own — everything here is a benchmark attribute — so it can be reordered or rebased onto `develop` without touching the other two. ## Changes Tagged: - `binary_ops`: the primitive arithmetic cases (`add_*`, `subtract_*`, `multiply_*`, `mul_*`, `div_i64_*`, `sub_i64_constant`, and the three `*_shapes` matrices) and the two primitive comparison cases (`eq_i64_constant`, `lt_i64_nullable`). - `compare`: `compare_int`, `compare_int_nullable`, `compare_int_constant`, `compare_int_eq`, `compare_float`. - `scalar_subtract`. - `lane_kernels`: `lanezip_checked_add_u32` and its `arrow_checked_add_u32` baseline. The baseline is tagged too — comparing the two is only meaningful under the same build flags. Left in simulation, with the reasoning recorded in each file's module docs: - Decimal arithmetic and comparison: `i128` widening and per-lane rescaling, not something a wider vector register decides. - Boolean `and`/`or`: already word-at-a-time over a bitmap. - String and struct comparison: dominated by view chasing and per-field dispatch. - Casts in `lane_kernels`: vectorization-sensitive, but out of scope here. Also left alone: the `between` benchmarks in `vortex-fastlanes` (`new_raw_prim_test_between` is a raw-primitive comparison kernel and does qualify) and the bit-packed comparison matrices. Both are `types =`/`consts =` parameterized, which `#[cpu_features]` has no coverage for yet, and both would fan out to dozens of walltime series. Worth a follow-up rather than a guess in this PR. Note that tagging moves a benchmark out of the sharded simulation job, so these series restart on the walltime legs instead of continuing their simulation history. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 0ef496c commit 17bd9e2

6 files changed

Lines changed: 60 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vortex-array/benches/binary_ops.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
//! Benchmarks for the binary `Operator` path, over primitive, decimal, and boolean inputs.
5+
//!
6+
//! The primitive arithmetic and comparison cases carry `#[cpu_features]`, so they are
7+
//! measured on every walltime CPU-feature leg rather than in simulation. Each is written
8+
//! once and compiled differently per leg: the kernel underneath is a portable lane loop, and
9+
//! how wide the compiler vectorizes it is decided by the build flags, not by the source.
10+
//!
11+
//! The decimal and boolean cases are not tagged. Decimal arithmetic is `i128` widening and
12+
//! per-lane rescaling, and the boolean kernels are already word-at-a-time over a bitmap;
13+
//! neither is where a wider vector register shows up. They stay in simulation.
14+
415
#![expect(clippy::unwrap_used)]
516
#![expect(
617
clippy::cast_possible_truncation,
@@ -73,16 +84,19 @@ enum BinaryShape {
7384
/// the instrumented CodSpeed runs quick.
7485
const DECIMAL_MUL_DIV_LEN: usize = 1_024;
7586

87+
#[vortex_bench_support::cpu_features]
7688
#[divan::bench(args = BINARY_SHAPE_CASES)]
7789
fn add_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) {
7890
bench_binary_shape(bencher, len, shape, Operator::Add);
7991
}
8092

93+
#[vortex_bench_support::cpu_features]
8194
#[divan::bench(args = BINARY_SHAPE_CASES)]
8295
fn subtract_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) {
8396
bench_binary_shape(bencher, len, shape, Operator::Sub);
8497
}
8598

99+
#[vortex_bench_support::cpu_features]
86100
#[divan::bench(args = BINARY_SHAPE_CASES)]
87101
fn multiply_shapes(bencher: Bencher, &(len, shape): &(usize, BinaryShape)) {
88102
bench_binary_shape(bencher, len, shape, Operator::Mul);
@@ -104,6 +118,7 @@ fn bench_binary_shape(bencher: Bencher, len: usize, shape: BinaryShape, operator
104118
bench_primitive(bencher, lhs, rhs, operator);
105119
}
106120

121+
#[vortex_bench_support::cpu_features]
107122
#[divan::bench]
108123
fn add_i64_nonnull(bencher: Bencher) {
109124
let lhs = primitive_nonnull(0, I64_LEN).into_array();
@@ -112,6 +127,7 @@ fn add_i64_nonnull(bencher: Bencher) {
112127
bench_primitive(bencher, lhs, rhs, Operator::Add);
113128
}
114129

130+
#[vortex_bench_support::cpu_features]
115131
#[divan::bench]
116132
fn add_i64_nullable(bencher: Bencher) {
117133
let lhs = primitive_nullable(0, 7, I64_LEN).into_array();
@@ -120,6 +136,7 @@ fn add_i64_nullable(bencher: Bencher) {
120136
bench_primitive(bencher, lhs, rhs, Operator::Add);
121137
}
122138

139+
#[vortex_bench_support::cpu_features]
123140
#[divan::bench]
124141
fn add_i64_constant(bencher: Bencher) {
125142
let lhs = primitive_nonnull(0, I64_LEN).into_array();
@@ -128,6 +145,7 @@ fn add_i64_constant(bencher: Bencher) {
128145
bench_primitive(bencher, lhs, rhs, Operator::Add);
129146
}
130147

148+
#[vortex_bench_support::cpu_features]
131149
#[divan::bench]
132150
fn add_i32_nonnull(bencher: Bencher) {
133151
let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array();
@@ -136,6 +154,7 @@ fn add_i32_nonnull(bencher: Bencher) {
136154
bench_primitive(bencher, lhs, rhs, Operator::Add);
137155
}
138156

157+
#[vortex_bench_support::cpu_features]
139158
#[divan::bench]
140159
fn add_u32_nonnull(bencher: Bencher) {
141160
let lhs = primitive_u32_small_nonnull(1, I32_LEN).into_array();
@@ -144,6 +163,7 @@ fn add_u32_nonnull(bencher: Bencher) {
144163
bench_primitive(bencher, lhs, rhs, Operator::Add);
145164
}
146165

166+
#[vortex_bench_support::cpu_features]
147167
#[divan::bench]
148168
fn mul_i64_nonnull(bencher: Bencher) {
149169
let lhs = primitive_small_nonnull(1, I64_LEN).into_array();
@@ -152,6 +172,7 @@ fn mul_i64_nonnull(bencher: Bencher) {
152172
bench_primitive(bencher, lhs, rhs, Operator::Mul);
153173
}
154174

175+
#[vortex_bench_support::cpu_features]
155176
#[divan::bench]
156177
fn mul_i8_nonnull(bencher: Bencher) {
157178
let lhs = primitive_i8_small_nonnull(1, I8_LEN).into_array();
@@ -160,6 +181,7 @@ fn mul_i8_nonnull(bencher: Bencher) {
160181
bench_primitive(bencher, lhs, rhs, Operator::Mul);
161182
}
162183

184+
#[vortex_bench_support::cpu_features]
163185
#[divan::bench]
164186
fn mul_u8_nonnull(bencher: Bencher) {
165187
let lhs = primitive_u8_small_nonnull(1, I8_LEN).into_array();
@@ -168,6 +190,7 @@ fn mul_u8_nonnull(bencher: Bencher) {
168190
bench_primitive(bencher, lhs, rhs, Operator::Mul);
169191
}
170192

193+
#[vortex_bench_support::cpu_features]
171194
#[divan::bench]
172195
fn mul_i16_nonnull(bencher: Bencher) {
173196
let lhs = primitive_i16_small_nonnull(1, I16_LEN).into_array();
@@ -176,6 +199,7 @@ fn mul_i16_nonnull(bencher: Bencher) {
176199
bench_primitive(bencher, lhs, rhs, Operator::Mul);
177200
}
178201

202+
#[vortex_bench_support::cpu_features]
179203
#[divan::bench]
180204
fn mul_u16_nonnull(bencher: Bencher) {
181205
let lhs = primitive_u16_small_nonnull(1, I16_LEN).into_array();
@@ -184,6 +208,7 @@ fn mul_u16_nonnull(bencher: Bencher) {
184208
bench_primitive(bencher, lhs, rhs, Operator::Mul);
185209
}
186210

211+
#[vortex_bench_support::cpu_features]
187212
#[divan::bench]
188213
fn mul_i32_nonnull(bencher: Bencher) {
189214
let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array();
@@ -192,6 +217,7 @@ fn mul_i32_nonnull(bencher: Bencher) {
192217
bench_primitive(bencher, lhs, rhs, Operator::Mul);
193218
}
194219

220+
#[vortex_bench_support::cpu_features]
195221
#[divan::bench]
196222
fn mul_u32_nonnull(bencher: Bencher) {
197223
let lhs = primitive_u32_small_nonnull(1, I32_LEN).into_array();
@@ -200,6 +226,7 @@ fn mul_u32_nonnull(bencher: Bencher) {
200226
bench_primitive(bencher, lhs, rhs, Operator::Mul);
201227
}
202228

229+
#[vortex_bench_support::cpu_features]
203230
#[divan::bench]
204231
fn mul_u64_nonnull(bencher: Bencher) {
205232
let lhs = primitive_u64_small_nonnull(1, I64_LEN).into_array();
@@ -208,6 +235,7 @@ fn mul_u64_nonnull(bencher: Bencher) {
208235
bench_primitive(bencher, lhs, rhs, Operator::Mul);
209236
}
210237

238+
#[vortex_bench_support::cpu_features]
211239
#[divan::bench]
212240
fn mul_i32_nullable(bencher: Bencher) {
213241
let lhs = primitive_i32_small_nullable(1, 7, I32_LEN).into_array();
@@ -216,6 +244,7 @@ fn mul_i32_nullable(bencher: Bencher) {
216244
bench_primitive(bencher, lhs, rhs, Operator::Mul);
217245
}
218246

247+
#[vortex_bench_support::cpu_features]
219248
#[divan::bench]
220249
fn mul_i32_constant(bencher: Bencher) {
221250
let lhs = primitive_i32_small_nonnull(1, I32_LEN).into_array();
@@ -224,6 +253,7 @@ fn mul_i32_constant(bencher: Bencher) {
224253
bench_primitive(bencher, lhs, rhs, Operator::Mul);
225254
}
226255

256+
#[vortex_bench_support::cpu_features]
227257
#[divan::bench]
228258
fn div_i64_nonnull(bencher: Bencher) {
229259
let lhs = primitive_nonnull(1_000_000, I64_LEN).into_array();
@@ -232,6 +262,7 @@ fn div_i64_nonnull(bencher: Bencher) {
232262
bench_primitive(bencher, lhs, rhs, Operator::Div);
233263
}
234264

265+
#[vortex_bench_support::cpu_features]
235266
#[divan::bench]
236267
fn div_i64_nullable(bencher: Bencher) {
237268
let lhs = primitive_nullable(1_000_000, 7, I64_LEN).into_array();
@@ -240,6 +271,7 @@ fn div_i64_nullable(bencher: Bencher) {
240271
bench_primitive(bencher, lhs, rhs, Operator::Div);
241272
}
242273

274+
#[vortex_bench_support::cpu_features]
243275
#[divan::bench]
244276
fn sub_i64_constant(bencher: Bencher) {
245277
let lhs = primitive_nonnull(0, I64_LEN).into_array();
@@ -296,6 +328,7 @@ fn div_decimal_i128_nullable(bencher: Bencher) {
296328
bench_decimal(bencher, lhs, rhs, Operator::Div);
297329
}
298330

331+
#[vortex_bench_support::cpu_features]
299332
#[divan::bench]
300333
fn eq_i64_constant(bencher: Bencher) {
301334
let lhs = primitive_nonnull(0, LEN).into_array();
@@ -304,6 +337,7 @@ fn eq_i64_constant(bencher: Bencher) {
304337
bench_bool(bencher, lhs, rhs, Operator::Eq);
305338
}
306339

340+
#[vortex_bench_support::cpu_features]
307341
#[divan::bench]
308342
fn lt_i64_nullable(bencher: Bencher) {
309343
let lhs = primitive_nullable(0, 7, LEN).into_array();

vortex-array/benches/compare.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33

44
//! Benchmarks for the binary comparison path, over every array kind it accepts.
55
//!
6-
//! `compare_int_constant_left`, `compare_u8`, `compare_u64`, and `compare_f32` carry
7-
//! `#[cpu_features]`, so they are measured on every walltime CPU-feature leg rather than in
8-
//! simulation. Each is written once and compiled differently per leg: today the primitive
9-
//! comparison path is a portable lane kernel, and how well it auto-vectorizes is decided by
10-
//! the build. That is the baseline a hand-written kernel selected through
11-
//! `cfg(target_feature)` has to beat, measured on the silicon it would run on.
6+
//! The primitive cases carry `#[cpu_features]`, so they are measured on every walltime
7+
//! CPU-feature leg rather than in simulation. Each is written once and compiled differently
8+
//! per leg: today the primitive comparison path is a portable lane kernel, and how well it
9+
//! auto-vectorizes is decided by the build. That is the baseline a hand-written kernel
10+
//! selected through `cfg(target_feature)` has to beat, measured on the silicon it would run
11+
//! on.
12+
//!
13+
//! The boolean, decimal, string, and struct cases are not tagged. A wider vector register is
14+
//! not what decides them: booleans are already word-at-a-time over a bitmap, decimals are
15+
//! `i128`, and the string and struct cases are dominated by view chasing and per-field
16+
//! dispatch. They stay in simulation.
1217
1318
#![expect(clippy::unwrap_used)]
1419

@@ -164,6 +169,7 @@ fn compare_bool_constant(bencher: Bencher) {
164169
bench_compare(bencher, arr, constant, Operator::Eq);
165170
}
166171

172+
#[vortex_bench_support::cpu_features]
167173
#[divan::bench]
168174
fn compare_int(bencher: Bencher) {
169175
let mut rng = StdRng::seed_from_u64(0);
@@ -172,6 +178,7 @@ fn compare_int(bencher: Bencher) {
172178
bench_compare(bencher, arr1, arr2, Operator::Gte);
173179
}
174180

181+
#[vortex_bench_support::cpu_features]
175182
#[divan::bench]
176183
fn compare_int_nullable(bencher: Bencher) {
177184
let mut rng = StdRng::seed_from_u64(0);
@@ -180,6 +187,7 @@ fn compare_int_nullable(bencher: Bencher) {
180187
bench_compare(bencher, arr1, arr2, Operator::Gte);
181188
}
182189

190+
#[vortex_bench_support::cpu_features]
183191
#[divan::bench]
184192
fn compare_int_constant(bencher: Bencher) {
185193
let mut rng = StdRng::seed_from_u64(0);
@@ -224,6 +232,7 @@ fn compare_f32(bencher: Bencher) {
224232
bench_compare(bencher, arr1, arr2, Operator::Gte);
225233
}
226234

235+
#[vortex_bench_support::cpu_features]
227236
#[divan::bench]
228237
fn compare_int_eq(bencher: Bencher) {
229238
let mut rng = StdRng::seed_from_u64(0);
@@ -232,6 +241,7 @@ fn compare_int_eq(bencher: Bencher) {
232241
bench_compare(bencher, arr1, arr2, Operator::Eq);
233242
}
234243

244+
#[vortex_bench_support::cpu_features]
235245
#[divan::bench]
236246
fn compare_float(bencher: Bencher) {
237247
let mut rng = StdRng::seed_from_u64(0);

vortex-array/benches/scalar_subtract.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn main() {
2929

3030
static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
3131

32+
#[vortex_bench_support::cpu_features]
3233
#[divan::bench]
3334
fn scalar_subtract(bencher: Bencher) {
3435
let mut rng = StdRng::seed_from_u64(0);

vortex-compute/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ arrow-schema = { workspace = true }
2828
divan = { workspace = true }
2929
num-traits = { workspace = true }
3030
rand = { workspace = true }
31+
vortex-bench-support = { workspace = true }
3132

3233
[lints]
3334
workspace = true

vortex-compute/benches/lane_kernels.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
//! Each Vortex kernel bench has a sibling `arrow_*` baseline bench using the
1313
//! equivalent arrow-rs kernel over the same data shape, so the divan report
1414
//! lines up side-by-side.
15+
//!
16+
//! The checked-add pair carries `#[cpu_features]`, so both are measured on every
17+
//! walltime CPU-feature leg rather than in simulation: they are one lane loop
18+
//! compiled differently per leg, and comparing them against arrow-rs is only
19+
//! meaningful under the same build flags. The cast benches stay in simulation.
1520
1621
#![expect(clippy::unwrap_used)]
1722
#![expect(clippy::clone_on_ref_ptr)]
@@ -324,6 +329,7 @@ fn add_fixture(n: usize) -> AddFixture {
324329
}
325330
}
326331

332+
#[vortex_bench_support::cpu_features]
327333
#[divan::bench(args = SIZES)]
328334
fn lanezip_checked_add_u32(bencher: Bencher, n: usize) {
329335
let f = add_fixture(n);
@@ -346,6 +352,7 @@ fn lanezip_checked_add_u32(bencher: Bencher, n: usize) {
346352
});
347353
}
348354

355+
#[vortex_bench_support::cpu_features]
349356
#[divan::bench(args = SIZES)]
350357
fn arrow_checked_add_u32(bencher: Bencher, n: usize) {
351358
let f = add_fixture(n);

0 commit comments

Comments
 (0)