Skip to content

Commit 57236cb

Browse files
committed
Add scalar function performance baselines
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent c2288dc commit 57236cb

16 files changed

Lines changed: 1155 additions & 23 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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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::ScalarFnArray;
24+
use vortex_array::arrays::VarBinViewArray;
25+
use vortex_array::scalar_fn::EmptyOptions;
26+
use vortex_array::scalar_fn::TypedScalarFnInstance;
27+
use vortex_array::scalar_fn::fns::byte_length::ByteLength;
28+
use vortex_array::validity::Validity;
29+
use vortex_session::VortexSession;
30+
31+
// Scalar function execution allocates its output inside the timed region, so use the vendored
32+
// allocator instead of measuring glibc differences between CodSpeed runner images.
33+
#[global_allocator]
34+
static GLOBAL: MiMalloc = MiMalloc;
35+
36+
static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
37+
38+
fn main() {
39+
LazyLock::force(&SESSION);
40+
divan::main();
41+
}
42+
43+
const SIZES: &[usize] = &[4_096, 65_536];
44+
45+
fn long_strings(len: usize) -> ArrayRef {
46+
VarBinViewArray::from_iter_str((0..len).map(|i| format!("a string well past inline: {i}")))
47+
.into_array()
48+
}
49+
50+
fn short_strings(len: usize) -> ArrayRef {
51+
VarBinViewArray::from_iter_str((0..len).map(|i| format!("{}", i % 1_000))).into_array()
52+
}
53+
54+
fn bench_byte_length(bencher: Bencher, input: ArrayRef) {
55+
let len = input.len();
56+
bencher
57+
.counter(ItemsCount::new(len))
58+
.with_inputs(|| {
59+
(
60+
ScalarFnArray::try_new(
61+
TypedScalarFnInstance::new(ByteLength, EmptyOptions).erased(),
62+
vec![input.clone()],
63+
)
64+
.unwrap()
65+
.into_array(),
66+
SESSION.create_execution_ctx(),
67+
)
68+
})
69+
.bench_values(|(array, mut ctx)| array.execute::<Canonical>(&mut ctx).unwrap());
70+
}
71+
72+
#[divan::bench(args = SIZES)]
73+
fn inline(bencher: Bencher, len: usize) {
74+
bench_byte_length(bencher, short_strings(len));
75+
}
76+
77+
#[divan::bench(args = SIZES)]
78+
fn out_of_line(bencher: Bencher, len: usize) {
79+
bench_byte_length(bencher, long_strings(len));
80+
}
81+
82+
#[divan::bench(args = SIZES)]
83+
fn nullable_out_of_line(bencher: Bencher, len: usize) {
84+
let validity = Validity::from_iter((0..len).map(|i| i % 8 != 0));
85+
let input = MaskedArray::try_new(long_strings(len), validity)
86+
.unwrap()
87+
.into_array();
88+
bench_byte_length(bencher, input);
89+
}
90+
91+
#[divan::bench(args = SIZES)]
92+
fn nullable_out_of_line_90pct(bencher: Bencher, len: usize) {
93+
let validity = Validity::from_iter((0..len).map(|i| i.is_multiple_of(10)));
94+
let input = MaskedArray::try_new(long_strings(len), validity)
95+
.unwrap()
96+
.into_array();
97+
bench_byte_length(bencher, input);
98+
}

‎vortex-array/benches/like.rs‎

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ use vortex_array::IntoArray;
1313
use vortex_array::VortexSessionExecute;
1414
use vortex_array::arrays::BoolArray;
1515
use vortex_array::arrays::ConstantArray;
16+
use vortex_array::arrays::ScalarFnArray;
1617
use vortex_array::arrays::VarBinViewArray;
17-
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
18+
use vortex_array::scalar_fn::TypedScalarFnInstance;
1819
use vortex_array::scalar_fn::fns::like::Like;
1920
use vortex_array::scalar_fn::fns::like::LikeOptions;
2021

@@ -24,11 +25,16 @@ fn main() {
2425

2526
const ARRAY_SIZE: usize = 2_048;
2627

28+
/// The per-row pattern arms compile one pattern per row in the worst case, which costs an order of
29+
/// magnitude more per row than matching against a constant pattern. They use a smaller fixture to
30+
/// stay inside the 1 ms per-iteration budget from `docs/developer-guide/benchmarking.md`.
31+
const PER_ROW_PATTERN_ROWS: usize = 512;
32+
2733
/// Random lowercase strings of 4..=24 bytes, some with a `hello` infix.
28-
fn strings() -> ArrayRef {
34+
fn strings(len: usize) -> ArrayRef {
2935
let mut rng = StdRng::seed_from_u64(0);
3036
let len_dist = Uniform::new_inclusive(4usize, 24).unwrap();
31-
VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|i| {
37+
VarBinViewArray::from_iter_str((0..len).map(|i| {
3238
let len = rng.sample(len_dist);
3339
let mut s: String = (0..len)
3440
.map(|_| char::from(rng.random_range(b'a'..=b'z')))
@@ -43,19 +49,19 @@ fn strings() -> ArrayRef {
4349

4450
fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) {
4551
let session = vortex_array::array_session();
46-
let array = strings();
52+
let array = strings(ARRAY_SIZE);
4753
bencher
4854
.with_inputs(|| {
4955
(
50-
Like.try_new_array(
51-
ARRAY_SIZE,
52-
options,
53-
[
56+
ScalarFnArray::try_new(
57+
TypedScalarFnInstance::new(Like, options).erased(),
58+
vec![
5459
array.clone(),
5560
ConstantArray::new(pattern, ARRAY_SIZE).into_array(),
5661
],
5762
)
58-
.unwrap(),
63+
.unwrap()
64+
.into_array(),
5965
session.create_execution_ctx(),
6066
)
6167
})
@@ -87,28 +93,63 @@ fn like_regex(bencher: Bencher) {
8793
bench_like(bencher, "h_llo%w%d", LikeOptions::default());
8894
}
8995

90-
#[divan::bench]
91-
fn like_per_row_patterns(bencher: Bencher) {
96+
fn bench_per_row_patterns(bencher: Bencher, patterns: ArrayRef) {
9297
let session = vortex_array::array_session();
93-
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();
98+
let array = strings(patterns.len());
9799
bencher
98100
.with_inputs(|| {
99101
(
100-
Like.try_new_array(
101-
ARRAY_SIZE,
102-
LikeOptions::default(),
103-
[array.clone(), patterns.clone()],
102+
ScalarFnArray::try_new(
103+
TypedScalarFnInstance::new(Like, LikeOptions::default()).erased(),
104+
vec![array.clone(), patterns.clone()],
104105
)
105-
.unwrap(),
106+
.unwrap()
107+
.into_array(),
106108
session.create_execution_ctx(),
107109
)
108110
})
109111
.bench_values(|(array, mut ctx)| array.execute::<BoolArray>(&mut ctx).unwrap());
110112
}
111113

114+
#[divan::bench]
115+
fn like_per_row_patterns(bencher: Bencher) {
116+
// A non-constant pattern child takes the per-row path; repeated patterns hit the compile cache.
117+
let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array();
118+
bench_per_row_patterns(bencher, patterns);
119+
}
120+
121+
/// The cached half of the compile-cache pair: every row repeats one pattern whose five-byte shape
122+
/// matches the distinct-pattern arm, so the two differ only in whether the cache hits. Both use
123+
/// [`PER_ROW_PATTERN_ROWS`] rows to stay comparable.
124+
///
125+
/// [`like_per_row_distinct_patterns`]
126+
#[divan::bench]
127+
fn like_per_row_repeated_patterns(bencher: Bencher) {
128+
let patterns =
129+
VarBinViewArray::from_iter_str((0..PER_ROW_PATTERN_ROWS).map(|_| "%aaa%")).into_array();
130+
bench_per_row_patterns(bencher, patterns);
131+
}
132+
133+
/// The per-row path with the compile cache defeated: every row carries a distinct pattern of the
134+
/// same shape as the repeated-pattern arm, so each row pays one pattern compilation.
135+
///
136+
/// [`like_per_row_repeated_patterns`]
137+
#[divan::bench]
138+
fn like_per_row_distinct_patterns(bencher: Bencher) {
139+
let patterns = VarBinViewArray::from_iter_str(
140+
(0..PER_ROW_PATTERN_ROWS).map(|i| format!("%{}%", distinct_trigram(i))),
141+
)
142+
.into_array();
143+
bench_per_row_patterns(bencher, patterns);
144+
}
145+
146+
/// A distinct three-letter lowercase infix per row, so every pattern has the same shape while
147+
/// no two rows share a compiled pattern.
148+
fn distinct_trigram(i: usize) -> String {
149+
let letter = |shift: usize| char::from(b'a' + u8::try_from((i >> shift) % 26).unwrap());
150+
[letter(0), letter(5), letter(10)].iter().collect()
151+
}
152+
112153
#[divan::bench]
113154
fn ilike_contains(bencher: Bencher) {
114155
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"] }
@@ -49,5 +50,13 @@ harness = false
4950
name = "predicate_bbox"
5051
harness = false
5152

53+
[[bench]]
54+
name = "binary_predicates"
55+
harness = false
56+
57+
[[bench]]
58+
name = "distance"
59+
harness = false
60+
5261
[lints]
5362
workspace = true

0 commit comments

Comments
 (0)