Skip to content

Commit 1dccaaf

Browse files
committed
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. 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 `ScalarFnFactoryExt` cannot represent its normalized-row invariant. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 0a92c58 commit 1dccaaf

15 files changed

Lines changed: 1101 additions & 5 deletions

File tree

‎.github/workflows/codspeed.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ jobs:
5757
- { shard: 6, name: "Encodings 3", packages: "vortex-pco vortex-runend vortex-sequence" }
5858
- { shard: 7, name: "Encodings 4", packages: "vortex-sparse vortex-zigzag vortex-zstd" }
5959
- { shard: 8, name: "Storage formats & row encoding", packages: "vortex-flatbuffers vortex-proto vortex-btrblocks vortex-row" }
60+
- { shard: 9, name: "Tensor & geo", packages: "vortex-tensor vortex-geo" }
6061
name: "Benchmark with Codspeed (Shard #${{ matrix.shard }})"
6162
timeout-minutes: 30
6263
runs-on: >-

‎Cargo.lock‎

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

‎vortex-array/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ harness = false
137137
name = "like"
138138
harness = false
139139

140+
[[bench]]
141+
name = "byte_length"
142+
harness = false
143+
140144
[[bench]]
141145
name = "interleave"
142146
harness = false

‎vortex-array/benches/binary_ops.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,30 @@ fn add_i64_nullable(bencher: Bencher) {
5454
bench_primitive(bencher, lhs, rhs, Operator::Add);
5555
}
5656

57+
#[divan::bench]
58+
fn add_i64_constant(bencher: Bencher) {
59+
let lhs = primitive_nonnull(0).into_array();
60+
let rhs = ConstantArray::new(1_000_000i64, LEN).into_array();
61+
62+
bench_primitive(bencher, lhs, rhs, Operator::Add);
63+
}
64+
65+
#[divan::bench]
66+
fn add_i32_nonnull(bencher: Bencher) {
67+
let lhs = primitive_i32_small_nonnull(1).into_array();
68+
let rhs = primitive_i32_small_nonnull(17).into_array();
69+
70+
bench_primitive(bencher, lhs, rhs, Operator::Add);
71+
}
72+
73+
#[divan::bench]
74+
fn add_u32_nonnull(bencher: Bencher) {
75+
let lhs = primitive_u32_small_nonnull(1).into_array();
76+
let rhs = primitive_u32_small_nonnull(17).into_array();
77+
78+
bench_primitive(bencher, lhs, rhs, Operator::Add);
79+
}
80+
5781
#[divan::bench]
5882
fn mul_i64_nonnull(bencher: Bencher) {
5983
let lhs = primitive_small_nonnull(1).into_array();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Baseline throughput for `byte_length` over UTF-8 view arrays.
5+
//!
6+
//! The arms cover inline and out-of-line views, plus nullable out-of-line views. Their names are
7+
//! intended to remain stable across scalar-function implementation changes so CodSpeed can compare
8+
//! them against `develop`.
9+
10+
#![expect(clippy::unwrap_used)]
11+
12+
use std::sync::LazyLock;
13+
14+
use divan::Bencher;
15+
use divan::counter::ItemsCount;
16+
use mimalloc::MiMalloc;
17+
use vortex_array::ArrayRef;
18+
use vortex_array::Canonical;
19+
use vortex_array::IntoArray;
20+
use vortex_array::VortexSessionExecute;
21+
use vortex_array::array_session;
22+
use vortex_array::arrays::MaskedArray;
23+
use vortex_array::arrays::VarBinViewArray;
24+
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
25+
use vortex_array::scalar_fn::EmptyOptions;
26+
use vortex_array::scalar_fn::fns::byte_length::ByteLength;
27+
use vortex_array::validity::Validity;
28+
use vortex_session::VortexSession;
29+
30+
// Scalar function execution allocates its output inside the timed region, so use the vendored
31+
// allocator instead of measuring glibc differences between CodSpeed runner images.
32+
#[global_allocator]
33+
static GLOBAL: MiMalloc = MiMalloc;
34+
35+
static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
36+
37+
fn main() {
38+
LazyLock::force(&SESSION);
39+
divan::main();
40+
}
41+
42+
const SIZES: &[usize] = &[4_096, 65_536];
43+
44+
fn long_strings(len: usize) -> ArrayRef {
45+
VarBinViewArray::from_iter_str((0..len).map(|i| format!("a string well past inline: {i}")))
46+
.into_array()
47+
}
48+
49+
fn short_strings(len: usize) -> ArrayRef {
50+
VarBinViewArray::from_iter_str((0..len).map(|i| format!("{}", i % 1_000))).into_array()
51+
}
52+
53+
fn bench_byte_length(bencher: Bencher, input: ArrayRef) {
54+
let len = input.len();
55+
bencher
56+
.counter(ItemsCount::new(len))
57+
.with_inputs(|| {
58+
(
59+
ByteLength
60+
.try_new_array(len, EmptyOptions, [input.clone()])
61+
.unwrap(),
62+
SESSION.create_execution_ctx(),
63+
)
64+
})
65+
.bench_values(|(array, mut ctx)| array.execute::<Canonical>(&mut ctx).unwrap());
66+
}
67+
68+
#[divan::bench(args = SIZES)]
69+
fn inline(bencher: Bencher, len: usize) {
70+
bench_byte_length(bencher, short_strings(len));
71+
}
72+
73+
#[divan::bench(args = SIZES)]
74+
fn out_of_line(bencher: Bencher, len: usize) {
75+
bench_byte_length(bencher, long_strings(len));
76+
}
77+
78+
#[divan::bench(args = SIZES)]
79+
fn nullable_out_of_line(bencher: Bencher, len: usize) {
80+
let validity = Validity::from_iter((0..len).map(|i| i % 8 != 0));
81+
let input = MaskedArray::try_new(long_strings(len), validity)
82+
.unwrap()
83+
.into_array();
84+
bench_byte_length(bencher, input);
85+
}
86+
87+
#[divan::bench(args = SIZES)]
88+
fn nullable_out_of_line_90pct(bencher: Bencher, len: usize) {
89+
let validity = Validity::from_iter((0..len).map(|i| i.is_multiple_of(10)));
90+
let input = MaskedArray::try_new(long_strings(len), validity)
91+
.unwrap()
92+
.into_array();
93+
bench_byte_length(bencher, input);
94+
}

‎vortex-array/benches/like.rs‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,9 @@ fn like_regex(bencher: Bencher) {
8787
bench_like(bencher, "h_llo%w%d", LikeOptions::default());
8888
}
8989

90-
#[divan::bench]
91-
fn like_per_row_patterns(bencher: Bencher) {
90+
fn bench_per_row_patterns(bencher: Bencher, patterns: ArrayRef) {
9291
let session = vortex_array::array_session();
9392
let array = strings();
94-
// A non-constant pattern child takes the per-row path; repeated patterns hit the
95-
// compile cache.
96-
let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array();
9793
bencher
9894
.with_inputs(|| {
9995
(
@@ -109,6 +105,32 @@ fn like_per_row_patterns(bencher: Bencher) {
109105
.bench_values(|(array, mut ctx)| array.execute::<BoolArray>(&mut ctx).unwrap());
110106
}
111107

108+
#[divan::bench]
109+
fn like_per_row_patterns(bencher: Bencher) {
110+
// A non-constant pattern child takes the per-row path; repeated patterns hit the compile
111+
// cache. Its five-byte `%aaa%` shape matches the distinct-pattern fixture below.
112+
let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "%aaa%")).into_array();
113+
bench_per_row_patterns(bencher, patterns);
114+
}
115+
116+
/// The per-row path with the compile cache defeated: every row carries a distinct pattern of the
117+
/// same shape, so each row pays one pattern compilation.
118+
#[divan::bench]
119+
fn like_per_row_distinct_patterns(bencher: Bencher) {
120+
let patterns = VarBinViewArray::from_iter_str(
121+
(0..ARRAY_SIZE).map(|i| format!("%{}%", distinct_trigram(i))),
122+
)
123+
.into_array();
124+
bench_per_row_patterns(bencher, patterns);
125+
}
126+
127+
/// A distinct three-letter lowercase infix per row, so every pattern has the same shape while
128+
/// no two rows share a compiled pattern.
129+
fn distinct_trigram(i: usize) -> String {
130+
let letter = |shift: usize| char::from(b'a' + u8::try_from((i >> shift) % 26).unwrap());
131+
[letter(0), letter(5), letter(10)].iter().collect()
132+
}
133+
112134
#[divan::bench]
113135
fn ilike_contains(bencher: Bencher) {
114136
bench_like(

‎vortex-geo/Cargo.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ _test-harness = []
3636

3737
[dev-dependencies]
3838
divan = { workspace = true }
39+
mimalloc = { workspace = true }
3940
rstest = { workspace = true }
4041
vortex-array = { workspace = true, features = ["_test-harness"] }
4142
vortex-geo = { path = ".", features = ["_test-harness"] }
@@ -45,5 +46,13 @@ vortex-layout = { workspace = true }
4546
name = "envelope"
4647
harness = false
4748

49+
[[bench]]
50+
name = "binary_predicates"
51+
harness = false
52+
53+
[[bench]]
54+
name = "distance"
55+
harness = false
56+
4857
[lints]
4958
workspace = true

0 commit comments

Comments
 (0)