Skip to content

Commit 9d3bd7d

Browse files
committed
feat: elias-fano encoding module ef and basic vortex vtable traits
Signed-off-by: rapour <reza.apour@gmail.com>
1 parent 75b6225 commit 9d3bd7d

24 files changed

Lines changed: 3963 additions & 0 deletions

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ members = [
6161
"encodings/bytebool",
6262
"encodings/parquet-variant",
6363
"encodings/onpair",
64+
"encodings/elias-fano",
6465
# Benchmarks
6566
"benchmarks/bench-support",
6667
"benchmarks/lance-bench",
@@ -312,6 +313,7 @@ vortex-datafusion = { version = "0.1.0", path = "./vortex-datafusion", default-f
312313
vortex-datetime-parts = { version = "0.1.0", path = "./encodings/datetime-parts", default-features = false }
313314
vortex-decimal-byte-parts = { version = "0.1.0", path = "encodings/decimal-byte-parts", default-features = false }
314315
vortex-edition = { version = "0.1.0", path = "./vortex-edition", default-features = false }
316+
vortex-elias-fano = { version = "0.1.0", path = "./encodings/elias-fano", default-features = false }
315317
vortex-error = { version = "0.1.0", path = "./vortex-error", default-features = false }
316318
vortex-fastlanes = { version = "0.1.0", path = "./encodings/fastlanes", default-features = false }
317319
vortex-file = { version = "0.1.0", path = "./vortex-file", default-features = false }

‎encodings/elias-fano/Cargo.toml‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[package]
2+
name = "vortex-elias-fano"
3+
authors = { workspace = true }
4+
categories = { workspace = true }
5+
description = "Vortex Elias-Fano encoded array for monotonic integer sequences"
6+
edition = { workspace = true }
7+
homepage = { workspace = true }
8+
include = { workspace = true }
9+
keywords = { workspace = true }
10+
license = { workspace = true }
11+
readme = { workspace = true }
12+
repository = { workspace = true }
13+
rust-version = { workspace = true }
14+
version = { workspace = true }
15+
16+
[dependencies]
17+
lending-iterator = { workspace = true }
18+
num-traits = { workspace = true }
19+
prost = { workspace = true }
20+
smallvec = { workspace = true }
21+
vortex-array = { workspace = true }
22+
vortex-buffer = { workspace = true }
23+
vortex-error = { workspace = true }
24+
vortex-fastlanes = { workspace = true }
25+
vortex-mask = { workspace = true }
26+
vortex-session = { workspace = true }
27+
28+
[dev-dependencies]
29+
divan = { workspace = true }
30+
rstest = { workspace = true }
31+
vortex-array = { path = "../../vortex-array", features = ["_test-harness"] }
32+
33+
[[bench]]
34+
name = "elias_fano"
35+
harness = false
36+
37+
[lints]
38+
workspace = true
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
��������� ���������� ���������(���������0���������

‎encodings/elias-fano/src/access.rs‎

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+
//! Binding the codec's random access to Vortex: describing an array's buffers as a layout,
5+
//! supplying the low bits out of the child array, and turning elements back into scalars.
6+
7+
use vortex_array::ArrayRef;
8+
use vortex_array::ArrayView;
9+
use vortex_array::ExecutionCtx;
10+
use vortex_array::scalar::Scalar;
11+
use vortex_error::VortexError;
12+
use vortex_error::VortexResult;
13+
use vortex_error::vortex_bail;
14+
use vortex_error::vortex_err;
15+
16+
use crate::EliasFano;
17+
use crate::array::EliasFanoSlotsView;
18+
use crate::array::scalar_from_bits;
19+
use crate::ef;
20+
use crate::ef::LowBits;
21+
use crate::malformed;
22+
23+
/// The low-bits child, as a source the codec can pull from.
24+
///
25+
/// Built per call rather than held: it borrows the execution context.
26+
pub(crate) struct LowerSource<'a, 'c> {
27+
pub(crate) lower: &'a ArrayRef,
28+
pub(crate) ctx: &'c mut ExecutionCtx,
29+
}
30+
31+
impl LowBits for LowerSource<'_, '_> {
32+
type Error = VortexError;
33+
34+
/// One low part, through the child's own `scalar_at`, so a rewritten slot needs no case here.
35+
fn get(&mut self, rank: u64) -> VortexResult<u64> {
36+
self.lower
37+
.execute_scalar(usize::try_from(rank)?, self.ctx)?
38+
.as_primitive()
39+
.typed_value::<u64>()
40+
.ok_or_else(|| vortex_err!("Elias-Fano low-bits child holds no value at rank {rank}"))
41+
}
42+
}
43+
44+
/// Carry a read's failure into Vortex's error type. A function rather than a `From` impl, for the
45+
/// reason [`crate::malformed`] gives.
46+
pub(crate) fn read_error(error: ef::ReadError<VortexError>) -> VortexError {
47+
match error {
48+
ef::ReadError::Malformed(error) => malformed(error),
49+
ef::ReadError::LowBits(error) => error,
50+
}
51+
}
52+
53+
/// The layout an array describes, borrowed from its buffers.
54+
///
55+
/// The upper array is taken as raw bytes rather than a `BitBuffer`, which would strip alignment and
56+
/// can reallocate to carry the three numbers the codec wants.
57+
pub(crate) fn layout<'a>(array: ArrayView<'a, EliasFano>) -> VortexResult<ef::Layout<'a>> {
58+
let data = array.data();
59+
let (_, samples1) = data.sample_bytes()?;
60+
let upper = ef::Bits::new(
61+
data.upper_buffer().as_slice(),
62+
0,
63+
usize::try_from(data.upper_len())?,
64+
);
65+
Ok(ef::Layout::new(
66+
upper,
67+
samples1,
68+
data.lower_width(),
69+
data.first_rank(),
70+
array.len(),
71+
))
72+
}
73+
74+
/// The value at logical `index`.
75+
pub(crate) fn access_at(
76+
array: ArrayView<'_, EliasFano>,
77+
index: usize,
78+
ctx: &mut ExecutionCtx,
79+
) -> VortexResult<Scalar> {
80+
let len = array.len();
81+
if index >= len {
82+
vortex_bail!(OutOfBounds: index, 0usize, len);
83+
}
84+
let reference_bits = array.data().reference_bits();
85+
let layout = layout(array)?;
86+
87+
// The slots view borrows the array behind the `ArrayView`; the `lower()` accessor would borrow
88+
// the (`Copy`, stack-local) view itself.
89+
let lower = EliasFanoSlotsView::from_slots(array.slots()).lower;
90+
let mut source = LowerSource { lower, ctx };
91+
92+
let element = ef::element_at(layout, index, &mut source).map_err(read_error)?;
93+
scalar_from_bits(array.dtype(), reference_bits.wrapping_add(element))
94+
}

0 commit comments

Comments
 (0)