|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +// SPDX-FileCopyrightText: Copyright the Vortex contributors |
| 3 | +// |
| 4 | +//! Microbenchmarks for the Elias-Fano array's read and write paths. |
| 5 | +//! |
| 6 | +//! `scalar_at` is the per-element path and reads the low-bits child once per probe, so it is what a |
| 7 | +//! change to that path has to be judged against. `encode` and `decode_bulk` cover the two batch |
| 8 | +//! paths. |
| 9 | +//! |
| 10 | +//! Three shapes, because the layout behaves differently in each: |
| 11 | +//! |
| 12 | +//! * `Sparse` — a wide universe, so `lower_width` is large and nearly every read touches the child. |
| 13 | +//! * `Dense` — a universe no wider than the row count, so `lower_width` is zero and the low-bits |
| 14 | +//! child is never read at all. The difference against `Sparse` is the child's whole cost. |
| 15 | +//! * `Duplicates` — few distinct values over a wide universe, so each occupied high-part bucket is |
| 16 | +//! deep. Random data almost never produces this. |
| 17 | +
|
| 18 | +#![allow( |
| 19 | + clippy::cast_possible_truncation, |
| 20 | + clippy::expect_used, |
| 21 | + clippy::tests_outside_test_module, |
| 22 | + clippy::unwrap_used |
| 23 | +)] |
| 24 | + |
| 25 | +use std::sync::LazyLock; |
| 26 | + |
| 27 | +use divan::Bencher; |
| 28 | +use vortex_array::ArrayRef; |
| 29 | +use vortex_array::IntoArray; |
| 30 | +use vortex_array::VortexSessionExecute; |
| 31 | +use vortex_array::arrays::Primitive; |
| 32 | +use vortex_array::arrays::PrimitiveArray; |
| 33 | +use vortex_elias_fano::EliasFanoArray; |
| 34 | +use vortex_elias_fano::elias_fano_encode; |
| 35 | +use vortex_session::VortexSession; |
| 36 | + |
| 37 | +static SESSION: LazyLock<VortexSession> = LazyLock::new(|| { |
| 38 | + let session = vortex_array::array_session(); |
| 39 | + vortex_fastlanes::initialize(&session); |
| 40 | + vortex_elias_fano::initialize(&session); |
| 41 | + session |
| 42 | +}); |
| 43 | + |
| 44 | +/// Deterministic xorshift, so a run is reproducible without a `rand` dependency. |
| 45 | +struct Rng(u64); |
| 46 | + |
| 47 | +impl Rng { |
| 48 | + fn next_u64(&mut self) -> u64 { |
| 49 | + self.0 ^= self.0 << 13; |
| 50 | + self.0 ^= self.0 >> 7; |
| 51 | + self.0 ^= self.0 << 17; |
| 52 | + self.0 |
| 53 | + } |
| 54 | + |
| 55 | + fn below(&mut self, bound: u64) -> u64 { |
| 56 | + self.next_u64() % bound |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 61 | +enum Shape { |
| 62 | + Sparse, |
| 63 | + Dense, |
| 64 | + Duplicates, |
| 65 | +} |
| 66 | + |
| 67 | +/// The universe every shape draws from, wide enough that `Sparse` gets ten low bits at 2^20 rows. |
| 68 | +const UNIVERSE: u64 = 1 << 30; |
| 69 | + |
| 70 | +/// Distinct values in the `Duplicates` shape: at 2^20 rows that is ~64 rows per value, so an |
| 71 | +/// occupied bucket is deep enough for `search_bucket` to bisect rather than walk. |
| 72 | +const LEVELS: u64 = 1 << 14; |
| 73 | + |
| 74 | +fn values(n: usize, shape: Shape) -> Vec<u64> { |
| 75 | + let mut rng = Rng(0x5EED_1234_ABCD_0001); |
| 76 | + let mut out: Vec<u64> = match shape { |
| 77 | + Shape::Sparse => (0..n).map(|_| rng.below(UNIVERSE)).collect(), |
| 78 | + // Universe == row count, which drives `lower_width` to zero. |
| 79 | + Shape::Dense => (0..n).map(|_| rng.below(n as u64)).collect(), |
| 80 | + Shape::Duplicates => { |
| 81 | + let step = UNIVERSE / LEVELS; |
| 82 | + (0..n).map(|_| rng.below(LEVELS) * step).collect() |
| 83 | + } |
| 84 | + }; |
| 85 | + out.sort_unstable(); |
| 86 | + out |
| 87 | +} |
| 88 | + |
| 89 | +fn encoded(n: usize, shape: Shape) -> EliasFanoArray { |
| 90 | + let array = PrimitiveArray::from_iter(values(n, shape)); |
| 91 | + let mut ctx = SESSION.create_execution_ctx(); |
| 92 | + elias_fano_encode(array.as_ref().as_::<Primitive>(), &mut ctx).expect("encode") |
| 93 | +} |
| 94 | + |
| 95 | +/// Probe count held fixed across shapes and row counts, so the reported figure is comparable. |
| 96 | +const PROBES: usize = 4096; |
| 97 | + |
| 98 | +const CASES: &[(Shape, usize)] = &[ |
| 99 | + (Shape::Sparse, 1 << 16), |
| 100 | + (Shape::Sparse, 1 << 20), |
| 101 | + (Shape::Dense, 1 << 20), |
| 102 | + (Shape::Duplicates, 1 << 20), |
| 103 | +]; |
| 104 | + |
| 105 | +fn random_indices(n: usize) -> Vec<usize> { |
| 106 | + let mut rng = Rng(0xA11C_E000_0000_0001); |
| 107 | + (0..PROBES).map(|_| rng.below(n as u64) as usize).collect() |
| 108 | +} |
| 109 | + |
| 110 | +/// Point lookups through `OperationsVTable::scalar_at`: one sampled `select1` and one low-bits read |
| 111 | +/// apiece, in a random order so nothing about locality is being measured by accident. |
| 112 | +#[divan::bench(args = CASES)] |
| 113 | +fn scalar_at(bencher: Bencher, case: (Shape, usize)) { |
| 114 | + let (shape, n) = case; |
| 115 | + let array: ArrayRef = encoded(n, shape).into_array(); |
| 116 | + let indices = random_indices(n); |
| 117 | + bencher |
| 118 | + .with_inputs(|| SESSION.create_execution_ctx()) |
| 119 | + .bench_local_values(|mut ctx| { |
| 120 | + for &index in &indices { |
| 121 | + divan::black_box(array.execute_scalar(index, &mut ctx).unwrap()); |
| 122 | + } |
| 123 | + }); |
| 124 | +} |
| 125 | + |
| 126 | +/// Whole-array decode, which walks the upper array once and reads the low bits a FastLanes block at |
| 127 | +/// a time rather than one element at a time. |
| 128 | +#[divan::bench(args = CASES)] |
| 129 | +fn decode_bulk(bencher: Bencher, case: (Shape, usize)) { |
| 130 | + let (shape, n) = case; |
| 131 | + let array = encoded(n, shape); |
| 132 | + bencher |
| 133 | + .with_inputs(|| (array.clone().into_array(), SESSION.create_execution_ctx())) |
| 134 | + .bench_local_values(|(array, mut ctx)| { |
| 135 | + divan::black_box(array.execute::<PrimitiveArray>(&mut ctx).unwrap()); |
| 136 | + }); |
| 137 | +} |
| 138 | + |
| 139 | +#[divan::bench(args = CASES)] |
| 140 | +fn encode(bencher: Bencher, case: (Shape, usize)) { |
| 141 | + let (shape, n) = case; |
| 142 | + let array = PrimitiveArray::from_iter(values(n, shape)); |
| 143 | + bencher |
| 144 | + .with_inputs(|| SESSION.create_execution_ctx()) |
| 145 | + .bench_local_values(|mut ctx| { |
| 146 | + divan::black_box( |
| 147 | + elias_fano_encode(array.as_ref().as_::<Primitive>(), &mut ctx).unwrap(), |
| 148 | + ); |
| 149 | + }); |
| 150 | +} |
| 151 | + |
| 152 | +fn main() { |
| 153 | + divan::main(); |
| 154 | +} |
0 commit comments