Skip to content

Commit 56eaf4b

Browse files
Cydhrafaassen
andauthored
Implemented Balanced Parenthesis (#23)
* implemented simple min-max-tree on a heap (internal) * implemented BP tree (open, close, enclose, tree navigation, level order navigation, subtree size, is_ancestor) * implemented builder struct for BP trees using depth-first traversal * implemented bp benchmark for non-trivial navigation functions * implemented test cases and fuzzing code * implemented lookup tables for block-search, both 8-bit and 16-bit version * implemented lookup tables answering the entire for block-search query for 8 bit * implement several iterators for DFS, sub-trees, and children * implemented From<BitVec> for BpTree * added a second job to the push workflow, which tests the library without crate features (i.e. without simd and bp_lookup_16) and without target-features Co-authored-by: Martijn Faassen <faassen@startifact.com>
1 parent 09e93f5 commit 56eaf4b

File tree

19 files changed

+3387
-83
lines changed

19 files changed

+3387
-83
lines changed

.github/workflows/rust.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,27 @@ on:
88

99
env:
1010
CARGO_TERM_COLOR: always
11-
RUSTFLAGS: -C target-cpu=native
11+
1212

1313
jobs:
1414
build:
15-
1615
runs-on: ubuntu-latest
17-
16+
env:
17+
RUSTFLAGS: -C target-cpu=native
1818
steps:
1919
- uses: actions/checkout@v4
2020
- name: Build
2121
run: cargo build --verbose --all-features
2222
- name: Run tests
2323
run: cargo test --verbose --all-features
24+
25+
test-fallbacks:
26+
runs-on: ubuntu-latest
27+
env:
28+
RUSTFLAGS: -C target-cpu=x86-64
29+
steps:
30+
- uses: actions/checkout@v4
31+
- name: Build
32+
run: cargo build --verbose --features serde
33+
- name: Run tests
34+
run: cargo test --verbose --features serde

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ rand = { version = "0.8", features = ["alloc"] }
2525

2626
[features]
2727
simd = []
28+
bp_u16_lookup = []
2829
docsrs = [] # special feature for docs.rs to enable doc_auto_cfg on nightly
2930

3031
[[bench]]
@@ -63,6 +64,10 @@ harness = false
6364
name = "rmq"
6465
harness = false
6566

67+
[[bench]]
68+
name = "bp"
69+
harness = false
70+
6671
[[bench]]
6772
name = "elias_fano_construction"
6873
harness = false

benches/bp.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#![allow(long_running_const_eval)]
2+
3+
use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
4+
use rand::rngs::StdRng;
5+
use rand::{Rng, SeedableRng};
6+
use std::cmp::Reverse;
7+
use std::collections::{BinaryHeap, HashSet};
8+
use vers_vecs::trees::bp::BpBuilder;
9+
use vers_vecs::trees::bp::BpTree;
10+
use vers_vecs::trees::{Tree, TreeBuilder};
11+
12+
mod common;
13+
14+
const BLOCK_SIZE: usize = 1024;
15+
16+
// TODO this function has nlogn runtime, which is a bit too much for the largest trees
17+
fn generate_tree<R: Rng>(rng: &mut R, nodes: u64) -> BpTree<BLOCK_SIZE> {
18+
// generate prüfer sequence
19+
let mut sequence = vec![0; (nodes - 2) as usize];
20+
for i in 0..nodes - 2 {
21+
sequence[i as usize] = rng.gen_range(0..nodes - 1);
22+
}
23+
24+
// decode prüfer sequence
25+
let mut degrees = vec![1; nodes as usize];
26+
sequence.iter().for_each(|i| degrees[*i as usize] += 1);
27+
28+
let mut prefix_sum = vec![0; nodes as usize];
29+
let mut sum = 0;
30+
degrees.iter().enumerate().for_each(|(i, d)| {
31+
prefix_sum[i] = sum;
32+
sum += d;
33+
});
34+
35+
let mut children = vec![0u64; sum];
36+
let mut assigned_children = vec![0; nodes as usize];
37+
38+
// keep a priority queue of nodes with degree one to reduce runtime from O(n^2) to O(n log n)
39+
let mut degree_one_set = BinaryHeap::new();
40+
degrees
41+
.iter()
42+
.enumerate()
43+
.filter(|(_, &v)| v == 1)
44+
.for_each(|(idx, _)| degree_one_set.push(Reverse(idx as u64)));
45+
46+
sequence.iter().for_each(|&i| {
47+
let j = degree_one_set.pop().unwrap().0;
48+
children[prefix_sum[i as usize] + assigned_children[i as usize]] = j;
49+
children[prefix_sum[j as usize] + assigned_children[j as usize]] = i;
50+
degrees[i as usize] -= 1;
51+
if degrees[i as usize] == 1 {
52+
degree_one_set.push(Reverse(i))
53+
}
54+
55+
degrees[j as usize] -= 1;
56+
if degrees[j as usize] == 1 {
57+
degree_one_set.push(Reverse(j))
58+
}
59+
60+
assigned_children[i as usize] += 1;
61+
assigned_children[j as usize] += 1;
62+
});
63+
64+
assert_eq!(degrees.iter().sum::<usize>(), 2);
65+
let u = degree_one_set.pop().unwrap().0;
66+
let v = degree_one_set.pop().unwrap().0;
67+
68+
children[prefix_sum[u as usize] + assigned_children[u as usize]] = v;
69+
children[prefix_sum[v as usize] + assigned_children[v as usize]] = u;
70+
71+
// build tree
72+
let mut bpb = BpBuilder::with_capacity(nodes);
73+
let mut stack = Vec::new();
74+
let mut visited = HashSet::with_capacity(nodes as usize);
75+
visited.insert(0);
76+
stack.push((0, 0u64, true));
77+
while let Some((depth, node, enter)) = stack.pop() {
78+
if enter {
79+
bpb.enter_node();
80+
stack.push((depth, node, false));
81+
for child in children
82+
.iter()
83+
.take(*prefix_sum.get(node as usize + 1).unwrap_or(&children.len()))
84+
.skip(prefix_sum[node as usize])
85+
{
86+
if visited.insert(*child) {
87+
stack.push((depth + 1, *child, true))
88+
}
89+
}
90+
} else {
91+
bpb.leave_node();
92+
}
93+
}
94+
95+
bpb.build().unwrap()
96+
}
97+
98+
fn bench_navigation(b: &mut Criterion) {
99+
let mut group = b.benchmark_group("bp");
100+
group.plot_config(common::plot_config());
101+
102+
for l in common::SIZES {
103+
// fix the rng seed because the measurements depend on the input structure.
104+
// to make multiple runs of the benchmark comparable, we fix the seed.
105+
// this is only a valid approach to check for performance improvements, it may not give
106+
// an accurate summary of the library's runtime
107+
let mut rng = StdRng::from_seed([0; 32]);
108+
109+
let bp = generate_tree(&mut rng, l as u64);
110+
let node_handles = (0..l).map(|i| bp.node_handle(i)).collect::<Vec<_>>();
111+
112+
group.bench_with_input(BenchmarkId::new("parent", l), &l, |b, _| {
113+
b.iter_batched(
114+
|| node_handles[rng.gen_range(0..node_handles.len())],
115+
|h| black_box(bp.parent(h)),
116+
BatchSize::SmallInput,
117+
)
118+
});
119+
120+
group.bench_with_input(BenchmarkId::new("last_child", l), &l, |b, _| {
121+
b.iter_batched(
122+
|| node_handles[rng.gen_range(0..node_handles.len())],
123+
|h| black_box(bp.last_child(h)),
124+
BatchSize::SmallInput,
125+
)
126+
});
127+
128+
group.bench_with_input(BenchmarkId::new("next_sibling", l), &l, |b, _| {
129+
b.iter_batched(
130+
|| node_handles[rng.gen_range(0..node_handles.len())],
131+
|h| black_box(bp.next_sibling(h)),
132+
BatchSize::SmallInput,
133+
)
134+
});
135+
136+
group.bench_with_input(BenchmarkId::new("prev_sibling", l), &l, |b, _| {
137+
b.iter_batched(
138+
|| node_handles[rng.gen_range(0..node_handles.len())],
139+
|h| black_box(bp.previous_sibling(h)),
140+
BatchSize::SmallInput,
141+
)
142+
});
143+
}
144+
}
145+
146+
criterion_group!(benches, bench_navigation);
147+
criterion_main!(benches);

benches/sparse_equals.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn bench(b: &mut Criterion<TimeDiff>) {
4444

4545
for fill_factor in FILL_FACTORS {
4646
group.bench_with_input(
47-
BenchmarkId::new("sparse overhead equal", &fill_factor),
47+
BenchmarkId::new("sparse overhead equal", fill_factor),
4848
&fill_factor,
4949
|b, _| {
5050
b.iter_custom(|iters| {
@@ -69,7 +69,7 @@ fn bench(b: &mut Criterion<TimeDiff>) {
6969
);
7070

7171
group.bench_with_input(
72-
BenchmarkId::new("sparse overhead unequal", &fill_factor),
72+
BenchmarkId::new("sparse overhead unequal", fill_factor),
7373
&fill_factor,
7474
|b, _| {
7575
b.iter_custom(|iters| {

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ since the intrinsics speed up both `rank` and `select` operations by a factor of
1616
- An Elias-Fano encoding of monotone sequences supporting constant-time predecessor/successor queries.
1717
- Two Range Minimum Query vector structures for constant-time range minimum queries.
1818
- A Wavelet Matrix supporting `O(k)` rank, select, statistical, predecessor, and successor queries.
19+
- A succinct tree structure supporting level-ordered and depth-first-ordered tree navigation and subtree queries.
1920

2021
## Why Vers?
2122
- Vers is among the fastest publicly available bit vector implementations for rank and select operations.
@@ -33,6 +34,7 @@ It also enables a special iterator for the rank/select bit vector that uses vect
3334
The feature only works on nightly Rust.
3435
Enabling it on stable Rust is a no-op, because the required CPU features are not available there.
3536
- `serde`: Enables serialization and deserialization of the data structures using the `serde` crate.
37+
- `u16_lookup` Enables a larger lookup table for BP tree queries. The larger table requires 128 KiB instead of 4 KiB.
3638

3739
## Benchmarks
3840
I benchmarked the implementations against publicly available implementations of the same data structures.

src/bit_vec/fast_rs_vec/tests.rs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ fn test_random_data_rank() {
4545
let data_index = rnd_index / WORD_SIZE;
4646
let bit_index = rnd_index % WORD_SIZE;
4747

48-
for i in 0..data_index {
49-
expected_rank1 += data[i].count_ones() as usize;
50-
expected_rank0 += data[i].count_zeros() as usize;
48+
for v in data.iter().take(data_index) {
49+
expected_rank1 += v.count_ones() as usize;
50+
expected_rank0 += v.count_zeros() as usize;
5151
}
5252

5353
if bit_index > 0 {
54-
expected_rank1 += (data[data_index] & (1 << bit_index) - 1).count_ones() as usize;
55-
expected_rank0 += (!data[data_index] & (1 << bit_index) - 1).count_ones() as usize;
54+
expected_rank1 += (data[data_index] & ((1 << bit_index) - 1)).count_ones() as usize;
55+
expected_rank0 += (!data[data_index] & ((1 << bit_index) - 1)).count_ones() as usize;
5656
}
5757

5858
assert_eq!(actual_rank1, expected_rank1);
@@ -503,14 +503,14 @@ fn test_custom_iter_behavior() {
503503
assert!(iter.advance_by(6).is_err());
504504
assert!(iter.advance_back_by(5).is_ok());
505505

506-
assert_eq!(rs.iter().skip(2).next(), Some(0));
506+
assert_eq!(rs.iter().nth(2), Some(0));
507507
assert_eq!(rs.iter().count(), 10);
508508
assert_eq!(rs.iter().skip(2).count(), 8);
509509
assert_eq!(rs.iter().last(), Some(0));
510510
assert_eq!(rs.iter().nth(3), Some(1));
511511
assert_eq!(rs.iter().nth(12), None);
512512

513-
assert_eq!(rs.clone().into_iter().skip(2).next(), Some(0));
513+
assert_eq!(rs.clone().into_iter().nth(2), Some(0));
514514
assert_eq!(rs.clone().into_iter().count(), 10);
515515
assert_eq!(rs.clone().into_iter().skip(2).count(), 8);
516516
assert_eq!(rs.clone().into_iter().last(), Some(0));
@@ -1093,21 +1093,21 @@ fn test_sparse_equals() {
10931093
let rs1 = RsVec::from_bit_vec(bv.clone());
10941094
let rs2 = RsVec::from_bit_vec(bv.clone());
10951095

1096-
assert_eq!(rs1.sparse_equals::<false>(&rs2), true);
1097-
assert_eq!(rs1.sparse_equals::<true>(&rs2), true);
1096+
assert!(rs1.sparse_equals::<false>(&rs2));
1097+
assert!(rs1.sparse_equals::<true>(&rs2));
10981098

10991099
bv.flip_bit(3);
11001100
let rs2 = RsVec::from_bit_vec(bv.clone());
11011101

1102-
assert_eq!(rs1.sparse_equals::<false>(&rs2), false);
1103-
assert_eq!(rs1.sparse_equals::<true>(&rs2), false);
1102+
assert!(!rs1.sparse_equals::<false>(&rs2));
1103+
assert!(!rs1.sparse_equals::<true>(&rs2));
11041104

11051105
bv.flip_bit(3);
11061106
bv.flip_bit(2 * SUPER_BLOCK_SIZE - 1);
11071107
let rs1 = RsVec::from_bit_vec(bv.clone());
11081108

1109-
assert_eq!(rs1.sparse_equals::<false>(&rs2), false);
1110-
assert_eq!(rs1.sparse_equals::<true>(&rs2), false);
1109+
assert!(!rs1.sparse_equals::<false>(&rs2));
1110+
assert!(!rs1.sparse_equals::<true>(&rs2));
11111111
}
11121112

11131113
#[test]
@@ -1137,18 +1137,18 @@ fn test_full_equals() {
11371137
let rs1 = RsVec::from_bit_vec(bv.clone());
11381138
let rs2 = RsVec::from_bit_vec(bv.clone());
11391139

1140-
assert_eq!(rs1.full_equals(&rs2), true);
1140+
assert!(rs1.full_equals(&rs2));
11411141

11421142
bv.flip_bit(3);
11431143
let rs2 = RsVec::from_bit_vec(bv.clone());
11441144

1145-
assert_eq!(rs1.full_equals(&rs2), false);
1145+
assert!(!rs1.full_equals(&rs2));
11461146

11471147
bv.flip_bit(3);
11481148
bv.flip_bit(2 * SUPER_BLOCK_SIZE - 1);
11491149
let rs1 = RsVec::from_bit_vec(bv.clone());
11501150

1151-
assert_eq!(rs1.full_equals(&rs2), false);
1151+
assert!(!rs1.full_equals(&rs2));
11521152
}
11531153

11541154
// fuzzing test for iter1 and iter0 as last ditch fail-safe
@@ -1332,7 +1332,7 @@ fn test_iter1_regression_i8() {
13321332
let mut bv = BitVec::from_zeros(8193);
13331333

13341334
for idx in &input_on_bits {
1335-
bv.set(*idx as usize, 1).unwrap();
1335+
bv.set(*idx, 1).unwrap();
13361336
}
13371337

13381338
let bv = RsVec::from_bit_vec(bv);

src/bit_vec/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ impl BitVec {
535535
return;
536536
}
537537

538-
let new_limb_count = (self.len - n + WORD_SIZE - 1) / WORD_SIZE;
538+
let new_limb_count = (self.len - n).div_ceil(WORD_SIZE);
539539

540540
// cut off limbs that we no longer need
541541
if new_limb_count < self.data.len() {
@@ -1019,11 +1019,12 @@ impl BitVec {
10191019
pub fn count_ones(&self) -> u64 {
10201020
let mut ones: u64 = self.data[0..self.len / WORD_SIZE]
10211021
.iter()
1022-
.map(|limb| limb.count_ones() as u64)
1022+
.map(|limb| u64::from(limb.count_ones()))
10231023
.sum();
10241024
if self.len % WORD_SIZE > 0 {
1025-
ones += (self.data.last().unwrap() & ((1 << (self.len % WORD_SIZE)) - 1)).count_ones()
1026-
as u64;
1025+
ones += u64::from(
1026+
(self.data.last().unwrap() & ((1 << (self.len % WORD_SIZE)) - 1)).count_ones(),
1027+
);
10271028
}
10281029
ones
10291030
}

src/bit_vec/tests.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,18 @@ fn test_custom_iter_behavior() {
213213
assert!(iter.advance_by(6).is_err());
214214
assert!(iter.advance_back_by(5).is_ok());
215215

216-
assert_eq!(bv.iter().skip(2).next(), Some(0));
216+
#[allow(clippy::iter_skip_next)]
217+
let next = bv.iter().skip(2).next(); // explicit test for skip()
218+
assert_eq!(next, Some(0));
217219
assert_eq!(bv.iter().count(), 10);
218220
assert_eq!(bv.iter().skip(2).count(), 8);
219221
assert_eq!(bv.iter().last(), Some(0));
220222
assert_eq!(bv.iter().nth(3), Some(1));
221223
assert_eq!(bv.iter().nth(12), None);
222224

223-
assert_eq!(bv.clone().into_iter().skip(2).next(), Some(0));
225+
#[allow(clippy::iter_skip_next)]
226+
let next = bv.clone().into_iter().skip(2).next(); // explicit test for skip()
227+
assert_eq!(next, Some(0));
224228
assert_eq!(bv.clone().into_iter().count(), 10);
225229
assert_eq!(bv.clone().into_iter().skip(2).count(), 8);
226230
assert_eq!(bv.clone().into_iter().last(), Some(0));

0 commit comments

Comments
 (0)