vers_vecs/bit_vec/fast_rs_vec/mod.rs
1//! A fast succinct bit vector implementation with rank and select queries. Rank computes in
2//! constant-time, select on average in constant-time, with a logarithmic worst case.
3
4use std::mem::size_of;
5
6#[cfg(all(
7 feature = "simd",
8 target_arch = "x86_64",
9 target_feature = "avx",
10 target_feature = "avx2",
11 target_feature = "avx512f",
12 target_feature = "avx512bw",
13))]
14pub use bitset::*;
15pub use iter::*;
16
17use crate::util::impl_vector_iterator;
18use crate::BitVec;
19
20use super::WORD_SIZE;
21
22/// Size of a block in the bitvector.
23const BLOCK_SIZE: usize = 512;
24
25/// Size of a super block in the bitvector. Super-blocks exist to decrease the memory overhead
26/// of block descriptors.
27/// Increasing or decreasing the super block size has negligible effect on performance of rank
28/// instruction. This means we want to make the super block size as large as possible, as long as
29/// the zero-counter in normal blocks still fits in a reasonable amount of bits. However, this has
30/// impact on the performance of select queries. The larger the super block size, the deeper will
31/// a binary search be. We found 2^13 to be a good compromise between memory overhead and
32/// performance.
33const SUPER_BLOCK_SIZE: usize = 1 << 13;
34
35/// Size of a select block. The select block is used to speed up select queries. The select block
36/// contains the indices of every `SELECT_BLOCK_SIZE`'th 1-bit and 0-bit in the bitvector.
37/// The smaller this block-size, the faster are select queries, but the more memory is used.
38const SELECT_BLOCK_SIZE: usize = 1 << 13;
39
40/// Meta-data for a block. The `zeros` field stores the number of zeros up to the block,
41/// beginning from the last super-block boundary. This means the first block in a super-block
42/// always stores the number zero, which serves as a sentinel value to avoid special-casing the
43/// first block in a super-block (which would be a performance hit due branch prediction failures).
44#[derive(Clone, Copy, Debug)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemSize, mem_dbg::MemDbg))]
47#[cfg_attr(feature = "mem_dbg", mem_size(flat))]
48struct BlockDescriptor {
49 zeros: u16,
50}
51
52/// Meta-data for a super-block. The `zeros` field stores the number of zeros up to this super-block.
53/// This allows the `BlockDescriptor` to store the number of zeros in a much smaller
54/// space. The `zeros` field is the number of zeros up to the super-block.
55#[derive(Clone, Copy, Debug)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemSize, mem_dbg::MemDbg))]
58#[cfg_attr(feature = "mem_dbg", mem_size(flat))]
59struct SuperBlockDescriptor {
60 zeros: usize,
61}
62
63/// Meta-data for the select query. Each entry i in the select vector contains the indices to find
64/// the i * `SELECT_BLOCK_SIZE`'th 0- and 1-bit in the bitvector. Those indices may be very far apart.
65/// The indices do not point into the bit-vector, but into the super-block vector.
66#[derive(Clone, Debug)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemSize, mem_dbg::MemDbg))]
69#[cfg_attr(feature = "mem_dbg", mem_size(flat))]
70struct SelectSuperBlockDescriptor {
71 index_0: usize,
72 index_1: usize,
73}
74
75/// A bitvector that supports constant-time rank and select queries and is optimized for fast queries.
76/// The bitvector is stored as a vector of `u64`s. The bit-vector stores meta-data for constant-time
77/// rank and select queries, which takes sub-linear additional space. The space overhead is
78/// 28 bits per 512 bits of user data (~5.47%).
79///
80/// # Example
81/// ```rust
82/// use vers_vecs::{BitVec, RsVec};
83///
84/// let mut bit_vec = BitVec::new();
85/// bit_vec.append_word(u64::MAX);
86///
87/// let rs_vec = RsVec::from_bit_vec(bit_vec);
88/// assert_eq!(rs_vec.rank1(64), 64);
89/// assert_eq!(rs_vec.select1(64), 64);
90///```
91#[derive(Clone, Debug)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[cfg_attr(feature = "mem_dbg", derive(mem_dbg::MemSize, mem_dbg::MemDbg))]
94pub struct RsVec {
95 data: Vec<u64>,
96 len: usize,
97 blocks: Vec<BlockDescriptor>,
98 super_blocks: Vec<SuperBlockDescriptor>,
99 select_blocks: Vec<SelectSuperBlockDescriptor>,
100 pub(crate) rank0: usize,
101 pub(crate) rank1: usize,
102}
103
104impl RsVec {
105 /// Build an `RsVec` from a [`BitVec`]. This will consume the `BitVec`. Since `RsVec`s are
106 /// immutable, this is the only way to construct an `RsVec`.
107 ///
108 /// # Example
109 /// See the example for `RsVec`.
110 ///
111 /// [`BitVec`]: BitVec
112 #[must_use]
113 pub fn from_bit_vec(vec: BitVec) -> RsVec {
114 // Construct the block descriptor meta data. Each block descriptor contains the number of
115 // zeros in the super-block, up to but excluding the block.
116 let mut blocks = Vec::with_capacity(vec.len() / BLOCK_SIZE + 1);
117 let mut super_blocks = Vec::with_capacity(vec.len() / SUPER_BLOCK_SIZE + 1);
118 let mut select_blocks = Vec::new();
119
120 // sentinel value
121 select_blocks.push(SelectSuperBlockDescriptor {
122 index_0: 0,
123 index_1: 0,
124 });
125
126 let mut total_zeros: usize = 0;
127 let mut current_zeros: usize = 0;
128 let mut last_zero_select_block: usize = 0;
129 let mut last_one_select_block: usize = 0;
130
131 for (idx, &word) in vec.data.iter().enumerate() {
132 // if we moved past a block boundary, append the block information for the previous
133 // block and reset the counter if we moved past a super-block boundary.
134 if idx % (BLOCK_SIZE / WORD_SIZE) == 0 {
135 if idx % (SUPER_BLOCK_SIZE / WORD_SIZE) == 0 {
136 total_zeros += current_zeros;
137 current_zeros = 0;
138 super_blocks.push(SuperBlockDescriptor { zeros: total_zeros });
139 }
140
141 // this cannot overflow because a super block isn't 2^16 bits long
142 #[allow(clippy::cast_possible_truncation)]
143 blocks.push(BlockDescriptor {
144 zeros: current_zeros as u16,
145 });
146 }
147
148 // count the zeros in the current word and add them to the counter
149 // the last word may contain padding zeros, which should not be counted,
150 // but since we do not append the last block descriptor, this is not a problem
151 let mut new_zeros = word.count_zeros() as usize;
152
153 // in the last block, remove remaining zeros of limb that aren't part of the vector
154 if idx == vec.data.len() - 1 && !vec.len.is_multiple_of(WORD_SIZE) {
155 let mask = (1 << (vec.len % WORD_SIZE)) - 1;
156 new_zeros -= (word | mask).count_zeros() as usize;
157 }
158
159 let all_zeros = total_zeros + current_zeros + new_zeros;
160 if all_zeros / SELECT_BLOCK_SIZE > (total_zeros + current_zeros) / SELECT_BLOCK_SIZE {
161 if all_zeros / SELECT_BLOCK_SIZE == select_blocks.len() {
162 select_blocks.push(SelectSuperBlockDescriptor {
163 index_0: super_blocks.len() - 1,
164 index_1: 0,
165 });
166 } else {
167 select_blocks[all_zeros / SELECT_BLOCK_SIZE].index_0 = super_blocks.len() - 1;
168 }
169
170 last_zero_select_block += 1;
171 }
172
173 let total_bits = (idx + 1) * WORD_SIZE;
174 let all_ones = total_bits - all_zeros;
175 if all_ones / SELECT_BLOCK_SIZE
176 > (idx * WORD_SIZE - total_zeros - current_zeros) / SELECT_BLOCK_SIZE
177 {
178 if all_ones / SELECT_BLOCK_SIZE == select_blocks.len() {
179 select_blocks.push(SelectSuperBlockDescriptor {
180 index_0: 0,
181 index_1: super_blocks.len() - 1,
182 });
183 } else {
184 select_blocks[all_ones / SELECT_BLOCK_SIZE].index_1 = super_blocks.len() - 1;
185 }
186
187 last_one_select_block += 1;
188 }
189
190 current_zeros += new_zeros;
191 }
192
193 // insert dummy select blocks at the end that just report the block beyond the number of super block
194 // this is a sentinel value that can be used as an upper bound for select.
195 // this would fail if select attempted to search a value outside the vector.
196 if last_zero_select_block == select_blocks.len() - 1 {
197 select_blocks.push(SelectSuperBlockDescriptor {
198 index_0: super_blocks.len(),
199 index_1: 0,
200 });
201 } else {
202 debug_assert_eq!(select_blocks[last_zero_select_block + 1].index_0, 0);
203 select_blocks[last_zero_select_block + 1].index_0 = super_blocks.len();
204 }
205 if last_one_select_block == select_blocks.len() - 1 {
206 select_blocks.push(SelectSuperBlockDescriptor {
207 index_0: 0,
208 index_1: super_blocks.len(),
209 });
210 } else {
211 debug_assert_eq!(select_blocks[last_one_select_block + 1].index_1, 0);
212 select_blocks[last_one_select_block + 1].index_1 = super_blocks.len();
213 }
214
215 total_zeros += current_zeros;
216
217 RsVec {
218 data: vec.data,
219 len: vec.len,
220 blocks,
221 super_blocks,
222 select_blocks,
223 rank0: total_zeros,
224 rank1: vec.len - total_zeros,
225 }
226 }
227
228 /// Return the 0-rank of the bit at the given position. The 0-rank is the number of
229 /// 0-bits in the vector up to but excluding the bit at the given position. Calling this
230 /// function with an index larger than the length of the bit-vector will report the total
231 /// number of 0-bits in the bit-vector.
232 ///
233 /// # Parameters
234 /// - `pos`: The position of the bit to return the rank of.
235 #[must_use]
236 pub fn rank0(&self, pos: usize) -> usize {
237 self.rank(true, pos)
238 }
239
240 /// Return the 1-rank of the bit at the given position. The 1-rank is the number of
241 /// 1-bits in the vector up to but excluding the bit at the given position. Calling this
242 /// function with an index larger than the length of the bit-vector will report the total
243 /// number of 1-bits in the bit-vector.
244 ///
245 /// # Parameters
246 /// - `pos`: The position of the bit to return the rank of.
247 #[must_use]
248 pub fn rank1(&self, pos: usize) -> usize {
249 self.rank(false, pos)
250 }
251
252 // I measured 5-10% improvement with this. I don't know why it's not inlined by default, the
253 // branch elimination profits alone should make it worth it.
254 #[allow(clippy::inline_always)]
255 #[inline(always)]
256 fn rank(&self, zero: bool, pos: usize) -> usize {
257 #[allow(clippy::collapsible_else_if)]
258 // readability and more obvious where dead branch elimination happens
259 if zero {
260 if pos >= self.len() {
261 return self.rank0;
262 }
263 } else {
264 if pos >= self.len() {
265 return self.rank1;
266 }
267 }
268
269 let index = pos / WORD_SIZE;
270 let block_index = pos / BLOCK_SIZE;
271 let super_block_index = pos / SUPER_BLOCK_SIZE;
272 let mut rank = 0;
273
274 // at first add the number of zeros/ones before the current super block
275 rank += if zero {
276 self.super_blocks[super_block_index].zeros
277 } else {
278 (super_block_index * SUPER_BLOCK_SIZE) - self.super_blocks[super_block_index].zeros
279 };
280
281 // then add the number of zeros/ones before the current block
282 rank += if zero {
283 self.blocks[block_index].zeros as usize
284 } else {
285 ((block_index % (SUPER_BLOCK_SIZE / BLOCK_SIZE)) * BLOCK_SIZE)
286 - self.blocks[block_index].zeros as usize
287 };
288
289 // naive popcount of blocks
290 for &i in &self.data[(block_index * BLOCK_SIZE) / WORD_SIZE..index] {
291 rank += if zero {
292 i.count_zeros() as usize
293 } else {
294 i.count_ones() as usize
295 };
296 }
297
298 rank += if zero {
299 (!self.data[index] & ((1 << (pos % WORD_SIZE)) - 1)).count_ones() as usize
300 } else {
301 (self.data[index] & ((1 << (pos % WORD_SIZE)) - 1)).count_ones() as usize
302 };
303
304 rank
305 }
306
307 /// Return the length of the vector, i.e. the number of bits it contains.
308 #[must_use]
309 pub fn len(&self) -> usize {
310 self.len
311 }
312
313 /// Return whether the vector is empty.
314 #[must_use]
315 pub fn is_empty(&self) -> bool {
316 self.len() == 0
317 }
318
319 /// Return the bit at the given position. The bit takes the least significant
320 /// bit of the returned u64 word.
321 /// If the position is larger than the length of the vector, `None` is returned.
322 #[must_use]
323 pub fn get(&self, pos: usize) -> Option<u64> {
324 if pos >= self.len() {
325 None
326 } else {
327 Some(self.get_unchecked(pos))
328 }
329 }
330
331 /// Return the bit at the given position. The bit takes the least significant
332 /// bit of the returned u64 word.
333 ///
334 /// # Panics
335 /// This function may panic if `pos >= self.len()` (alternatively, it may return garbage).
336 #[must_use]
337 pub fn get_unchecked(&self, pos: usize) -> u64 {
338 (self.data[pos / WORD_SIZE] >> (pos % WORD_SIZE)) & 1
339 }
340
341 /// Return multiple bits at the given position. The number of bits to return is given by `len`.
342 /// At most 64 bits can be returned.
343 /// If the position at the end of the query is larger than the length of the vector,
344 /// None is returned (even if the query partially overlaps with the vector).
345 /// If the length of the query is larger than 64, None is returned.
346 #[must_use]
347 pub fn get_bits(&self, pos: usize, len: usize) -> Option<u64> {
348 if len > WORD_SIZE {
349 return None;
350 }
351 if pos + len > self.len {
352 None
353 } else {
354 Some(self.get_bits_unchecked(pos, len))
355 }
356 }
357
358 /// Return multiple bits at the given position. The number of bits to return is given by `len`.
359 /// At most 64 bits can be returned.
360 ///
361 /// This function is always inlined, because it gains a lot from loop optimization and
362 /// can utilize the processor pre-fetcher better if it is.
363 ///
364 /// # Errors
365 /// If the length of the query is larger than 64, unpredictable data will be returned.
366 /// Use [`get_bits`] to properly handle this case with an `Option`.
367 ///
368 /// # Panics
369 /// If the position or interval is larger than the length of the vector,
370 /// the function will either return unpredictable data, or panic.
371 ///
372 /// [`get_bits`]: #method.get_bits
373 #[must_use]
374 #[allow(clippy::comparison_chain)] // readability
375 #[allow(clippy::cast_possible_truncation)] // parameter must be out of scope for this to happen
376 pub fn get_bits_unchecked(&self, pos: usize, len: usize) -> u64 {
377 debug_assert!(len <= WORD_SIZE);
378 let partial_word = self.data[pos / WORD_SIZE] >> (pos % WORD_SIZE);
379 if pos % WORD_SIZE + len <= WORD_SIZE {
380 partial_word & 1u64.checked_shl(len as u32).unwrap_or(0).wrapping_sub(1)
381 } else {
382 (partial_word | (self.data[pos / WORD_SIZE + 1] << (WORD_SIZE - pos % WORD_SIZE)))
383 & 1u64.checked_shl(len as u32).unwrap_or(0).wrapping_sub(1)
384 }
385 }
386
387 /// Convert the `RsVec` into a [`BitVec`].
388 /// This consumes the `RsVec`, and discards all meta-data.
389 /// Since [`RsVec`]s are innately immutable, this conversion is the only way to modify the
390 /// underlying data.
391 ///
392 /// # Example
393 /// ```rust
394 /// use vers_vecs::{BitVec, RsVec};
395 ///
396 /// let mut bit_vec = BitVec::new();
397 /// bit_vec.append_word(u64::MAX);
398 ///
399 /// let rs_vec = RsVec::from_bit_vec(bit_vec);
400 /// assert_eq!(rs_vec.rank1(64), 64);
401 ///
402 /// let mut bit_vec = rs_vec.into_bit_vec();
403 /// bit_vec.flip_bit(32);
404 /// let rs_vec = RsVec::from_bit_vec(bit_vec);
405 /// assert_eq!(rs_vec.rank1(64), 63);
406 /// assert_eq!(rs_vec.select0(0), 32);
407 /// ```
408 #[must_use]
409 pub fn into_bit_vec(self) -> BitVec {
410 BitVec {
411 data: self.data,
412 len: self.len,
413 }
414 }
415
416 /// Check if two `RsVec`s are equal. For sparse vectors (either sparsely filled with 1-bits or
417 /// 0-bits), this is faster than comparing the vectors bit by bit.
418 /// Choose the value of `ZERO` depending on which bits are more sparse.
419 ///
420 /// This method is faster than [`full_equals`] for sparse vectors beginning at roughly 1
421 /// million bits. Above 4 million bits, this method becomes faster than full equality in general.
422 ///
423 /// # Parameters
424 /// - `other`: The other `RsVec` to compare to.
425 /// - `ZERO`: Whether to compare the sparse 0-bits (true) or the sparse 1-bits (false).
426 ///
427 /// # Returns
428 /// `true` if the vectors' contents are equal, `false` otherwise.
429 ///
430 /// [`full_equals`]: RsVec::full_equals
431 #[must_use]
432 pub fn sparse_equals<const ZERO: bool>(&self, other: &Self) -> bool {
433 if self.len() != other.len() {
434 return false;
435 }
436
437 if self.rank0 != other.rank0 || self.rank1 != other.rank1 {
438 return false;
439 }
440
441 let iter: SelectIter<ZERO> = self.select_iter();
442
443 for (rank, bit_index) in iter.enumerate() {
444 // since rank is inlined, we get dead code elimination depending on ZERO
445 if (other.get_unchecked(bit_index) == 0) != ZERO || other.rank(ZERO, bit_index) != rank
446 {
447 return false;
448 }
449 }
450
451 true
452 }
453
454 /// Check if two `RsVec`s are equal. This compares limb by limb. This is usually faster than a
455 /// [`sparse_equals`] call for small vectors.
456 ///
457 /// # Parameters
458 /// - `other`: The other `RsVec` to compare to.
459 ///
460 /// # Returns
461 /// `true` if the vectors' contents are equal, `false` otherwise.
462 ///
463 /// [`sparse_equals`]: RsVec::sparse_equals
464 #[must_use]
465 pub fn full_equals(&self, other: &Self) -> bool {
466 if self.len() != other.len() {
467 return false;
468 }
469
470 if self.rank0 != other.rank0 || self.rank1 != other.rank1 {
471 return false;
472 }
473
474 if self.data[..self.len / 64]
475 .iter()
476 .zip(other.data[..other.len / 64].iter())
477 .any(|(a, b)| a != b)
478 {
479 return false;
480 }
481
482 // if last incomplete block exists, test it without junk data
483 if !self.len.is_multiple_of(WORD_SIZE)
484 && self.data[self.len / WORD_SIZE] & ((1 << (self.len % WORD_SIZE)) - 1)
485 != other.data[self.len / WORD_SIZE] & ((1 << (other.len % WORD_SIZE)) - 1)
486 {
487 return false;
488 }
489
490 true
491 }
492
493 /// Returns the number of bytes used on the heap for this vector. This does not include
494 /// allocated space that is not used (e.g. by the allocation behavior of `Vec`).
495 #[must_use]
496 pub fn heap_size(&self) -> usize {
497 self.data.len() * size_of::<u64>()
498 + self.blocks.len() * size_of::<BlockDescriptor>()
499 + self.super_blocks.len() * size_of::<SuperBlockDescriptor>()
500 + self.select_blocks.len() * size_of::<SelectSuperBlockDescriptor>()
501 }
502}
503
504impl_vector_iterator! { RsVec, RsVecIter, RsVecRefIter }
505
506impl PartialEq for RsVec {
507 /// Check if two `RsVec`s are equal. This method calls [`sparse_equals`] if the vector has more
508 /// than 4'000'000 bits, and [`full_equals`] otherwise.
509 ///
510 /// This was determined with benchmarks on an `x86_64` machine,
511 /// on which [`sparse_equals`] outperforms [`full_equals`] consistently above this threshold.
512 ///
513 /// # Parameters
514 /// - `other`: The other `RsVec` to compare to.
515 ///
516 /// # Returns
517 /// `true` if the vectors' contents are equal, `false` otherwise.
518 ///
519 /// [`sparse_equals`]: RsVec::sparse_equals
520 /// [`full_equals`]: RsVec::full_equals
521 fn eq(&self, other: &Self) -> bool {
522 if self.len > 4_000_000 {
523 if self.rank1 > self.rank0 {
524 self.sparse_equals::<true>(other)
525 } else {
526 self.sparse_equals::<false>(other)
527 }
528 } else {
529 self.full_equals(other)
530 }
531 }
532}
533
534impl From<BitVec> for RsVec {
535 /// Build an [`RsVec`] from a [`BitVec`]. This will consume the [`BitVec`]. Since [`RsVec`]s are
536 /// immutable, this is the only way to construct an [`RsVec`].
537 ///
538 /// # Example
539 /// See the example for [`RsVec`].
540 ///
541 /// [`BitVec`]: BitVec
542 /// [`RsVec`]: RsVec
543 fn from(vec: BitVec) -> Self {
544 RsVec::from_bit_vec(vec)
545 }
546}
547
548impl From<RsVec> for BitVec {
549 fn from(value: RsVec) -> Self {
550 value.into_bit_vec()
551 }
552}
553
554// iter code in here to keep it more organized
555mod iter;
556// select code in here to keep it more organized
557mod select;
558
559#[cfg(all(
560 feature = "simd",
561 target_arch = "x86_64",
562 target_feature = "avx",
563 target_feature = "avx2",
564 target_feature = "avx512f",
565 target_feature = "avx512bw",
566))]
567mod bitset;
568
569#[cfg(test)]
570mod tests;