Skip to content

Commit 4c49d32

Browse files
committed
perf(fastlanes): Refine constant list membership
Signed-off-by: Will Manning <will@willmanning.io>
1 parent 1e34544 commit 4c49d32

10 files changed

Lines changed: 628 additions & 215 deletions

File tree

encodings/fastlanes/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ harness = false
5151
[[bench]]
5252
name = "bitpacking_list_contains"
5353
harness = false
54-
required-features = ["_test-harness"]
5554

5655
[[bench]]
5756
name = "canonicalize_bench"
Lines changed: 103 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! Measures the linear-search and binary-search crossover for constant-list membership.
4+
//! Compares compressed list membership with the canonical fallback.
55
//!
6-
//! The strategy benchmarks isolate lookup cost across mostly-missing and mixed probes. The kernel
7-
//! benchmark includes scalar extraction, sorting, FastLanes decoding, and result construction.
8-
//! The isolated lookup benchmark excludes sorting, so it favors binary search. Use the forced
9-
//! full-kernel results to select the production strategy.
6+
//! The specialized session evaluates membership while it decodes FastLanes lanes. The fallback
7+
//! session decodes the complete array before the generic membership operation.
8+
//! Density cases stress the 4 KiB lookup-table boundary. Sparse cases exceed that boundary.
109
//!
1110
//! Run with `cargo bench -p vortex-fastlanes --bench bitpacking_list_contains`.
1211
@@ -22,100 +21,57 @@ use vortex_array::ArrayRef;
2221
use vortex_array::IntoArray;
2322
use vortex_array::VortexSessionExecute;
2423
use vortex_array::array_session;
25-
use vortex_array::arrays::ConstantArray;
24+
use vortex_array::arrays::BoolArray;
2625
use vortex_array::arrays::PrimitiveArray;
2726
use vortex_array::dtype::DType;
2827
use vortex_array::dtype::Nullability;
2928
use vortex_array::dtype::PType;
29+
use vortex_array::expr::list_contains;
30+
use vortex_array::expr::lit;
31+
use vortex_array::expr::root;
3032
use vortex_array::scalar::Scalar;
31-
use vortex_array::scalar_fn::fns::list_contains::ListContainsElementKernel;
33+
use vortex_array::session::ArraySessionExt;
3234
use vortex_array::validity::Validity;
3335
use vortex_buffer::Alignment;
3436
use vortex_buffer::BufferMut;
3537
use vortex_fastlanes::BitPacked;
3638
use vortex_fastlanes::BitPackedArray;
3739
use vortex_fastlanes::BitPackedData;
38-
use vortex_fastlanes::list_contains_test_harness;
39-
use vortex_fastlanes::list_contains_test_harness::MembershipSearch;
40-
41-
const LEN: usize = 64 * 1_024;
42-
const MEMBER_COUNTS: &[usize] = &[1, 2, 3, 4, 6, 8, 9, 12, 16, 24, 32, 64];
43-
const STRATEGY_MEMBER_COUNTS: &[usize] = &[3, 4, 6, 8, 9, 12, 16, 24, 32, 64];
40+
use vortex_session::VortexSession;
41+
42+
const DENSE_CASES: &[(usize, usize)] = &[
43+
(64, 1),
44+
(64, 4),
45+
(64, 8),
46+
(64, 32),
47+
(64, 64),
48+
(1_024, 1),
49+
(1_024, 4),
50+
(1_024, 8),
51+
(1_024, 32),
52+
(1_024, 64),
53+
(65_536, 1),
54+
(65_536, 4),
55+
(65_536, 8),
56+
(65_536, 32),
57+
(65_536, 64),
58+
];
59+
const SPARSE_CASES: &[(usize, usize)] = &[(1_024, 8), (1_024, 64), (65_536, 8), (65_536, 64)];
60+
const DENSITY_CASES: &[(usize, usize, u32)] = &[
61+
(64, 5, 1_000),
62+
(64, 8, 512),
63+
(64, 64, 64),
64+
(65_536, 5, 1_000),
65+
(65_536, 8, 512),
66+
(65_536, 64, 64),
67+
];
4468

4569
fn main() {
4670
divan::main();
4771
}
4872

49-
fn members(count: usize) -> Vec<u32> {
50-
(0..count).map(|index| index as u32 * 2).collect()
51-
}
52-
53-
fn mostly_missing_values() -> Vec<u32> {
54-
(0..LEN)
55-
.map(|index| ((index as u32 * 17) % 4_096) | 1)
56-
.collect()
57-
}
58-
59-
fn mixed_values(members: &[u32]) -> Vec<u32> {
60-
(0..LEN)
61-
.map(|index| {
62-
if index.is_multiple_of(2) {
63-
members[(index / 2) % members.len()]
64-
} else {
65-
((index as u32 * 17) % 4_096) | 1
66-
}
67-
})
68-
.collect()
69-
}
70-
71-
fn count_linear(values: &[u32], members: &[u32]) -> usize {
72-
values
73-
.iter()
74-
.filter(|value| members.contains(black_box(value)))
75-
.count()
76-
}
77-
78-
fn count_binary(values: &[u32], members: &[u32]) -> usize {
79-
values
80-
.iter()
81-
.filter(|value| members.binary_search(black_box(value)).is_ok())
82-
.count()
83-
}
84-
85-
#[divan::bench(args = MEMBER_COUNTS)]
86-
fn linear_mostly_missing(bencher: Bencher, member_count: usize) {
87-
let values = mostly_missing_values();
88-
let members = members(member_count);
89-
bencher
90-
.counter(ItemsCount::new(LEN))
91-
.bench_local(|| black_box(count_linear(&values, &members)));
92-
}
93-
94-
#[divan::bench(args = MEMBER_COUNTS)]
95-
fn binary_mostly_missing(bencher: Bencher, member_count: usize) {
96-
let values = mostly_missing_values();
97-
let members = members(member_count);
98-
bencher
99-
.counter(ItemsCount::new(LEN))
100-
.bench_local(|| black_box(count_binary(&values, &members)));
101-
}
102-
103-
#[divan::bench(args = MEMBER_COUNTS)]
104-
fn linear_mixed(bencher: Bencher, member_count: usize) {
105-
let members = members(member_count);
106-
let values = mixed_values(&members);
107-
bencher
108-
.counter(ItemsCount::new(LEN))
109-
.bench_local(|| black_box(count_linear(&values, &members)));
110-
}
111-
112-
#[divan::bench(args = MEMBER_COUNTS)]
113-
fn binary_mixed(bencher: Bencher, member_count: usize) {
114-
let members = members(member_count);
115-
let values = mixed_values(&members);
116-
bencher
117-
.counter(ItemsCount::new(LEN))
118-
.bench_local(|| black_box(count_binary(&values, &members)));
73+
fn members(count: usize, stride: u32) -> Vec<u32> {
74+
(0..count).map(|index| index as u32 * stride).collect()
11975
}
12076

12177
fn page_aligned(array: BitPackedArray) -> BitPackedArray {
@@ -133,9 +89,21 @@ fn page_aligned(array: BitPackedArray) -> BitPackedArray {
13389
.unwrap()
13490
}
13591

136-
fn kernel_inputs(member_count: usize) -> (BitPackedArray, ArrayRef) {
137-
let mut ctx = array_session().create_execution_ctx();
138-
let values: BufferMut<u32> = (0..LEN).map(|index| (index as u32 * 17) % 1_024).collect();
92+
fn benchmark_input(
93+
len: usize,
94+
member_count: usize,
95+
member_stride: u32,
96+
specialized: bool,
97+
) -> (ArrayRef, VortexSession) {
98+
let session = array_session();
99+
if specialized {
100+
vortex_fastlanes::initialize(&session);
101+
} else {
102+
session.arrays().register(BitPacked);
103+
}
104+
105+
let mut ctx = session.create_execution_ctx();
106+
let values: BufferMut<u32> = (0..len).map(|index| (index as u32 * 17) % 1_024).collect();
139107
let packed = page_aligned(
140108
BitPackedData::encode(
141109
&PrimitiveArray::new(values.freeze(), Validity::NonNullable).into_array(),
@@ -144,71 +112,62 @@ fn kernel_inputs(member_count: usize) -> (BitPackedArray, ArrayRef) {
144112
)
145113
.unwrap(),
146114
);
147-
let member_scalars = members(member_count)
115+
let member_scalars = members(member_count, member_stride)
148116
.into_iter()
149117
.map(|value| Scalar::primitive(value, Nullability::NonNullable))
150118
.collect();
151-
let list = ConstantArray::new(
152-
Scalar::list(
153-
Arc::new(DType::Primitive(PType::U32, Nullability::NonNullable)),
154-
member_scalars,
155-
Nullability::NonNullable,
156-
),
157-
LEN,
158-
)
159-
.into_array();
160-
(packed, list)
119+
let list = Scalar::list(
120+
Arc::new(DType::Primitive(PType::U32, Nullability::NonNullable)),
121+
member_scalars,
122+
Nullability::NonNullable,
123+
);
124+
let contains = packed
125+
.into_array()
126+
.apply(&list_contains(lit(list), root()))
127+
.unwrap();
128+
(contains, session)
161129
}
162130

163-
#[divan::bench(args = MEMBER_COUNTS)]
164-
fn bitpacked_kernel(bencher: Bencher, member_count: usize) {
165-
let (packed, list) = kernel_inputs(member_count);
166-
let mut ctx = array_session().create_execution_ctx();
167-
bencher.counter(ItemsCount::new(LEN)).bench_local(|| {
168-
black_box(
169-
<BitPacked as ListContainsElementKernel>::list_contains(
170-
&list,
171-
packed.as_view(),
172-
&mut ctx,
173-
)
174-
.unwrap()
175-
.unwrap(),
176-
)
177-
});
131+
fn bench_contains(
132+
bencher: Bencher,
133+
len: usize,
134+
member_count: usize,
135+
member_stride: u32,
136+
specialized: bool,
137+
) {
138+
let (contains, session) = benchmark_input(len, member_count, member_stride, specialized);
139+
let mut ctx = session.create_execution_ctx();
140+
bencher
141+
.counter(ItemsCount::new(len))
142+
.bench_local(|| black_box(contains.clone().execute::<BoolArray>(&mut ctx).unwrap()));
178143
}
179144

180-
#[divan::bench(args = STRATEGY_MEMBER_COUNTS)]
181-
fn bitpacked_linear(bencher: Bencher, member_count: usize) {
182-
let (packed, list) = kernel_inputs(member_count);
183-
let mut ctx = array_session().create_execution_ctx();
184-
bencher.counter(ItemsCount::new(LEN)).bench_local(|| {
185-
black_box(
186-
list_contains_test_harness::list_contains(
187-
&list,
188-
packed.as_view(),
189-
MembershipSearch::Linear,
190-
&mut ctx,
191-
)
192-
.unwrap()
193-
.unwrap(),
194-
)
195-
});
145+
#[divan::bench(args = DENSE_CASES)]
146+
fn compressed_dense(bencher: Bencher, (len, member_count): (usize, usize)) {
147+
bench_contains(bencher, len, member_count, 2, true);
196148
}
197149

198-
#[divan::bench(args = STRATEGY_MEMBER_COUNTS)]
199-
fn bitpacked_binary(bencher: Bencher, member_count: usize) {
200-
let (packed, list) = kernel_inputs(member_count);
201-
let mut ctx = array_session().create_execution_ctx();
202-
bencher.counter(ItemsCount::new(LEN)).bench_local(|| {
203-
black_box(
204-
list_contains_test_harness::list_contains(
205-
&list,
206-
packed.as_view(),
207-
MembershipSearch::Binary,
208-
&mut ctx,
209-
)
210-
.unwrap()
211-
.unwrap(),
212-
)
213-
});
150+
#[divan::bench(args = DENSE_CASES)]
151+
fn canonical_dense(bencher: Bencher, (len, member_count): (usize, usize)) {
152+
bench_contains(bencher, len, member_count, 2, false);
153+
}
154+
155+
#[divan::bench(args = SPARSE_CASES)]
156+
fn compressed_sparse(bencher: Bencher, (len, member_count): (usize, usize)) {
157+
bench_contains(bencher, len, member_count, 10_000, true);
158+
}
159+
160+
#[divan::bench(args = SPARSE_CASES)]
161+
fn canonical_sparse(bencher: Bencher, (len, member_count): (usize, usize)) {
162+
bench_contains(bencher, len, member_count, 10_000, false);
163+
}
164+
165+
#[divan::bench(args = DENSITY_CASES)]
166+
fn compressed_density(bencher: Bencher, (len, member_count, member_stride): (usize, usize, u32)) {
167+
bench_contains(bencher, len, member_count, member_stride, true);
168+
}
169+
170+
#[divan::bench(args = DENSITY_CASES)]
171+
fn canonical_density(bencher: Bencher, (len, member_count, member_stride): (usize, usize, u32)) {
172+
bench_contains(bencher, len, member_count, member_stride, false);
214173
}

0 commit comments

Comments
 (0)