Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- { shard: 5, name: "Core & encodings", packages: "vortex-buffer vortex-error vortex-mask vortex-compute vortex-file vortex-alp vortex-bytebool vortex-datetime-parts" }
- { shard: 6, name: "Main library & storage", packages: "vortex vortex-btrblocks vortex-compressor vortex-row" }
- { shard: 7, name: "FastLanes & decimals", packages: "vortex-decimal-byte-parts vortex-fastlanes", features: "--features _test-harness" }
- { shard: 8, name: "Encodings & layout", packages: "vortex-pco vortex-runend vortex-sequence vortex-sparse vortex-zigzag vortex-zstd vortex-layout" }
- { shard: 8, name: "Encodings & layout", packages: "vortex-elias-fano vortex-pco vortex-runend vortex-sequence vortex-sparse vortex-zigzag vortex-zstd vortex-layout" }
- { shard: 9, name: "Strings, tensor & spatial", packages: "vortex-fsst vortex-tensor vortex-spatial", features: "--features _test-harness" }
name: "Benchmark with Codspeed (Shard #${{ matrix.shard }} - ${{ matrix.name }})"
timeout-minutes: 30
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ members = [
"encodings/parquet-variant",
"encodings/onpair",
"encodings/uuid",
"encodings/elias-fano",
# Benchmarks
"benchmarks/bench-support",
"benchmarks/lance-bench",
Expand Down Expand Up @@ -317,6 +318,7 @@ vortex-datafusion = { version = "0.1.0", path = "./vortex-datafusion", default-f
vortex-datetime-parts = { version = "0.1.0", path = "./encodings/datetime-parts", default-features = false }
vortex-decimal-byte-parts = { version = "0.1.0", path = "encodings/decimal-byte-parts", default-features = false }
vortex-edition = { version = "0.1.0", path = "./vortex-edition", default-features = false }
vortex-elias-fano = { version = "0.1.0", path = "./encodings/elias-fano", default-features = false }
vortex-error = { version = "0.1.0", path = "./vortex-error", default-features = false }
vortex-fastlanes = { version = "0.1.0", path = "./encodings/fastlanes", default-features = false }
vortex-file = { version = "0.1.0", path = "./vortex-file", default-features = false }
Expand Down
35 changes: 35 additions & 0 deletions encodings/elias-fano/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "vortex-elias-fano"
authors = { workspace = true }
categories = { workspace = true }
description = "Vortex Elias-Fano and unary integer encodings"
edition = { workspace = true }
include = { workspace = true }
keywords = { workspace = true }
license = { workspace = true }
readme = { workspace = true }
repository = { workspace = true }
rust-version = { workspace = true }
version = { workspace = true }

[dependencies]
num-traits = { workspace = true }
prost = { workspace = true }
vortex-array = { workspace = true }
vortex-buffer = { workspace = true }
vortex-error = { workspace = true }
vortex-session = { workspace = true }

[dev-dependencies]
divan = { workspace = true }
mimalloc = { workspace = true }
rand = { workspace = true }
rstest = { workspace = true }
vortex-array = { workspace = true, features = ["_test-harness"] }

[[bench]]
name = "unary"
harness = false

[lints]
workspace = true
171 changes: 171 additions & 0 deletions encodings/elias-fano/benches/unary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

//! Baselines for the naive `Unary` encoding, so each later optimisation can be measured
//! against them. Keep the names and inputs stable: CodSpeed compares benchmarks by name.
//!
//! Each shape is what an Elias-Fano high part will hold for `LEN` sorted values: one count per
//! bucket of how many values fall in it. All four are small enough for the encoder's size rule:
//!
//! * `Dense`: values drawn from `[0, LEN)`, so about one value per bucket.
//! * `Spread`: values drawn from `[0, 2 * LEN)`, about half a value per bucket, the sparsest an
//! Elias-Fano high part gets.
//! * `Runs`: `LEN / 32` values, 32 rows each, so every count is 32 and set bits come in long runs.
//! * `Gap`: dense halves either side of one jump of `GAP` values, a long run of empty buckets that
//! every scan crossing it has to walk.

#![expect(clippy::unwrap_used)]

use std::sync::LazyLock;

use divan::Bencher;
use mimalloc::MiMalloc;
use rand::RngExt;
use rand::SeedableRng;
use rand::rngs::StdRng;
use vortex_array::ArrayRef;
use vortex_array::IntoArray;
use vortex_array::VortexSessionExecute;
use vortex_array::arrays::Primitive;
use vortex_array::arrays::PrimitiveArray;
use vortex_elias_fano::unary_encode;
use vortex_session::VortexSession;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
let session = vortex_array::array_session();
vortex_elias_fano::initialize(&session);
session
});

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Shape {
Dense,
Spread,
Runs,
Gap,
}

const SHAPES: &[Shape] = &[Shape::Dense, Shape::Spread, Shape::Runs, Shape::Gap];

/// Sorted values behind each shape, few enough that a whole decode stays inside the 1 ms
/// per-iteration budget.
const LEN: usize = 1 << 16;

/// The jump in `Gap`, in values.
const GAP: usize = LEN;

/// Rows per value in `Runs`. At 64, each count would take 65 bits, past the size rule.
const ROWS_PER_VALUE: usize = 32;

/// Probes per iteration. Each is a linear scan from the first bit, so 256 keep an iteration near
/// the budget, as in `vortex-array`'s probe benchmark.
const PROBES: usize = 256;

/// How many of `values` fall in each of `buckets` buckets, one value wide.
fn bucket_counts(values: impl Iterator<Item = usize>, buckets: usize) -> Vec<u64> {
let mut counts = vec![0; buckets];
for value in values {
counts[value] += 1;
}
counts
}

fn counts(shape: Shape) -> Vec<u64> {
let mut rng = StdRng::seed_from_u64(0);
match shape {
Shape::Dense => bucket_counts((0..LEN).map(|_| rng.random_range(0..LEN)), LEN),
Shape::Spread => bucket_counts((0..LEN).map(|_| rng.random_range(0..2 * LEN)), 2 * LEN),
Shape::Runs => bucket_counts(
(0..LEN).map(|row| row / ROWS_PER_VALUE),
LEN / ROWS_PER_VALUE,
),
Shape::Gap => bucket_counts(
(0..LEN).map(|row| {
let offset = if row < LEN / 2 { 0 } else { GAP };
offset + rng.random_range(0..LEN / 2)
}),
GAP + LEN / 2,
),
}
}

fn primitive(shape: Shape) -> PrimitiveArray {
PrimitiveArray::from_iter(counts(shape))
}

fn encoded(shape: Shape) -> ArrayRef {
unary_encode(primitive(shape).as_ref().as_::<Primitive>())
.unwrap()
.unwrap()
.into_array()
}

#[divan::bench(args = SHAPES)]
fn encode(bencher: Bencher, shape: Shape) {
let array = primitive(shape);
bencher.bench_local(|| {
divan::black_box(unary_encode(array.as_ref().as_::<Primitive>()).unwrap());
});
}

/// The whole array, in one walk over the unset bits.
#[divan::bench(args = SHAPES)]
fn decode(bencher: Bencher, shape: Shape) {
let array = encoded(shape);
bencher
.with_inputs(|| (array.clone(), SESSION.create_execution_ctx()))
.bench_local_values(|(array, mut ctx)| {
divan::black_box(array.execute::<PrimitiveArray>(&mut ctx).unwrap());
});
}

/// Point reads in a random order, each a scan from the first bit.
#[divan::bench(args = SHAPES)]
fn scalar_at_random(bencher: Bencher, shape: Shape) {
let array = encoded(shape);
let mut rng = StdRng::seed_from_u64(1);
let indices: Vec<usize> = (0..PROBES)
.map(|_| rng.random_range(0..array.len()))
.collect();
bencher
.with_inputs(|| SESSION.create_execution_ctx())
.bench_local_values(|mut ctx| {
for &index in &indices {
divan::black_box(array.execute_scalar(index, &mut ctx).unwrap());
}
});
}

/// Consecutive point reads from the middle, which a cursor could answer from the last position.
#[divan::bench(args = SHAPES)]
fn scalar_at_sequential(bencher: Bencher, shape: Shape) {
let array = encoded(shape);
let start = array.len() / 2;
bencher
.with_inputs(|| SESSION.create_execution_ctx())
.bench_local_values(|mut ctx| {
for index in start..start + PROBES {
divan::black_box(array.execute_scalar(index, &mut ctx).unwrap());
}
});
}

/// The middle half, through the default slice path, which decodes the whole array first.
#[divan::bench(args = SHAPES)]
fn slice_decode(bencher: Bencher, shape: Shape) {
let array = encoded(shape);
let len = array.len();
bencher
.with_inputs(|| (array.clone(), SESSION.create_execution_ctx()))
.bench_local_values(|(array, mut ctx)| {
let sliced = array.slice(len / 4..3 * len / 4).unwrap();
divan::black_box(sliced.execute::<PrimitiveArray>(&mut ctx).unwrap());
});
}

fn main() {
divan::main();
}
1 change: 1 addition & 0 deletions encodings/elias-fano/goldenfiles/unary.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ÿÿÿÿÿÿÿÿÿ
27 changes: 27 additions & 0 deletions encodings/elias-fano/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

//! Elias-Fano and unary encodings for integers.
//!
//! [`Unary`] stores a non-nullable unsigned column as one bit array, writing each value in unary.
//! It takes `len + num_ones` bits, where `num_ones` is the sum of the values, so it suits small
//! values in any order. It is also meant to hold the bucket counts of the high bits of the
//! Elias-Fano encoding this crate is being built towards.
//!
//! None of these encodings is in a file edition or registered by `vortex-file`, so the default
//! writers reject them for now. [`initialize`] registers them in a session.
#![deny(missing_docs)]

mod unary;

pub use unary::Unary;
pub use unary::UnaryArray;
pub use unary::UnaryData;
pub use unary::unary_encode;
use vortex_array::session::ArraySessionExt;
use vortex_session::VortexSession;

/// Register this crate's encodings in `session`.
pub fn initialize(session: &VortexSession) {
session.arrays().register(Unary);
}
Loading
Loading