A header-only C++20 hierarchical bitset library with fast successor/predecessor queries.
hibitset::bitset<U> stores a set of indices in hibitset, generalized to arbitrary universe sizes with the layer count derived at compile time.
#include <hibitset/hibitset.hpp>
hibitset::bitset<262144> bitset; // universe [0, 262144), 3 layers
bitset.set(50'000);
bitset.set(50'250);
bitset.test(50'000); // true
bitset.next_set(50'001); // 50250 (first set bit >= 50001)
bitset.prev_set(50'249); // 50000 (last set bit <= 50249)
bitset.next_set(60'000); // hibitset::bitset<262144>::npos (none found)
bitset.count(); // 2
for (auto p : bitset) {} // iterates 50000 and 50250 in order
bitset.clear(50'000);Since all operations are constexpr, the whole structure can be built and queried at compile time.
The traditional bitsets find the successor/predecessor of a certain index by performing a linear scan, which is computationally unfeasible for applications that have large universe sizes and require fast iteration. hibitset solves this problem by creating summary layers that allows the set to skip chunks that doesn't contain any set bits. In the worst case scenario, finding the next/prev set bit will require an ascend and descend through
In terms of memory, hibitset uses a fixed
Benchmark results show that hibitset provides a significant speedup over std::set and boost::dynamic_bitset (dynset) in successor/predecessor queries, while staying in a competitive range of memory usage. While hibitset supports algebraic operations, algebra-heavy applications may prefer dynset for better performance in set algebra operations.
The benchmarks are run on Apple M3, Apple Clang 17.0.0, -O3 -DNDEBUG (CMake Release), single-threaded, macOS.
The results can be reproduced by running:
cmake -B build-bench -S . -DCMAKE_BUILD_TYPE=Release -DHIBITSET_BUILD_BENCHMARKS=ON
cmake --build build-bench -j
./build-bench/benchmarks/hibitset_bench| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 1.46 | π₯ 16.5 | π₯ 3.75 |
| 2^12 (4,096) | 50% | π₯ 0.95 | π₯ 27.8 | π₯ 4.66 |
| 2^12 (4,096) | 95% | π₯ 0.81 | π₯ 34.4 | π₯ 1.46 |
| 2^18 (262,144) | 5% | π₯ 1.82 | π₯ 45.0 | π₯ 4.06 |
| 2^18 (262,144) | 50% | π₯ 0.92 | π₯ 70.9 | π₯ 4.95 |
| 2^18 (262,144) | 95% | π₯ 0.85 | π₯ 83.8 | π₯ 1.44 |
| 2^24 (16,777,216) | 5% | π₯ 3.92 | π₯ 149.2 | π₯ 5.13 |
| 2^24 (16,777,216) | 50% | π₯ 1.53 | π₯ 382.7 | π₯ 5.75 |
| 2^24 (16,777,216) | 95% | π₯ 1.37 | π₯ 445.7 | π₯ 1.73 |
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 1.51 | π₯ 28.2 | π₯ 14.6 |
| 2^12 (4,096) | 50% | π₯ 0.89 | π₯ 42.3 | π₯ 7.15 |
| 2^12 (4,096) | 95% | π₯ 0.80 | π₯ 50.9 | π₯ 1.15 |
| 2^18 (262,144) | 5% | π₯ 3.18 | π₯ 64.6 | π₯ 15.2 |
| 2^18 (262,144) | 50% | π₯ 1.43 | π₯ 93.3 | π₯ 7.11 |
| 2^18 (262,144) | 95% | π₯ 1.34 | π₯ 108.9 | π₯ 1.16 |
| 2^24 (16,777,216) | 5% | π₯ 3.78 | π₯ 234.0 | π₯ 18.6 |
| 2^24 (16,777,216) | 50% | π₯ 1.56 | π₯ 467.8 | π₯ 8.40 |
| 2^24 (16,777,216) | 95% | π₯ 1.35 | π₯ 543.1 | π₯ 1.52 |
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 1.60 | π₯ 2.49 | π₯ 3.49 |
| 2^12 (4,096) | 50% | π₯ 0.68 | π₯ 2.60 | π₯ 3.59 |
| 2^12 (4,096) | 95% | π₯ 0.67 | π₯ 4.85 | π₯ 3.48 |
| 2^18 (262,144) | 5% | π₯ 2.19 | π₯ 7.92 | π₯ 3.60 |
| 2^18 (262,144) | 50% | π₯ 0.83 | π₯ 12.3 | π₯ 3.56 |
| 2^18 (262,144) | 95% | π₯ 0.66 | π₯ 15.8 | π₯ 3.46 |
| 2^24 (16,777,216) | 5% | π₯ 3.04 | π₯ 60.5 | π₯ 5.13 |
| 2^24 (16,777,216) | 50% | π₯ 0.86 | π₯ 72.3 | π₯ 3.58 |
| 2^24 (16,777,216) | 95% | π₯ 0.70 | π₯ 74.7 | π₯ 3.49 |
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 51.6 | π₯ 2,457 | π₯ 7.12 |
| 2^12 (4,096) | 50% | π₯ 35.9 | π₯ 66,933 | π₯ 7.01 |
| 2^12 (4,096) | 95% | π₯ 35.9 | π₯ 251,416 | π₯ 7.01 |
| 2^18 (262,144) | 5% | π₯ 6,994 | π₯ 236,871 | π₯ 417.6 |
| 2^18 (262,144) | 50% | π₯ 8,958 | π₯ 9,500,833 | π₯ 417.0 |
| 2^18 (262,144) | 95% | π₯ 8,964 | π₯ 31,829,500 | π₯ 417.1 |
| 2^24 (16,777,216) | 5% | π₯ 387,865 | π₯ 90,221,000 | π₯ 45,770 |
| 2^24 (16,777,216) | 50% | π₯ 576,934 | π₯ 1,569,192,000 | π₯ 46,475 |
| 2^24 (16,777,216) | 95% | π₯ 578,616 | π₯ 4,136,211,000 | π₯ 46,012 |
hibitset is better at OR than AND and XOR because the resulting summary layers can be trivially computed from the summary layers of the operands, while AND and XOR require a rebuild of the summary layers from the base layer.
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 13.3 | π₯ 19,372 | π₯ 7.22 |
| 2^12 (4,096) | 50% | π₯ 13.2 | π₯ 193,502 | π₯ 7.07 |
| 2^12 (4,096) | 95% | π₯ 13.2 | π₯ 274,056 | π₯ 7.38 |
| 2^18 (262,144) | 5% | π₯ 970.9 | π₯ 2,024,000 | π₯ 419.4 |
| 2^18 (262,144) | 50% | π₯ 979.6 | π₯ 22,428,667 | π₯ 420.9 |
| 2^18 (262,144) | 95% | π₯ 980.2 | π₯ 35,328,000 | π₯ 418.2 |
| 2^24 (16,777,216) | 5% | π₯ 82,102 | π₯ 270,159,000 | π₯ 46,146 |
| 2^24 (16,777,216) | 50% | π₯ 82,350 | π₯ 2,623,463,000 | π₯ 55,512 |
| 2^24 (16,777,216) | 95% | π₯ 81,664 | π₯ 4,372,417,000 | π₯ 46,342 |
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 35.3 | π₯ 18,597 | π₯ 7.05 |
| 2^12 (4,096) | 50% | π₯ 36.0 | π₯ 119,992 | π₯ 7.02 |
| 2^12 (4,096) | 95% | π₯ 36.0 | π₯ 36,276 | π₯ 7.01 |
| 2^18 (262,144) | 5% | π₯ 8,803 | π₯ 1,872,135 | π₯ 417.6 |
| 2^18 (262,144) | 50% | π₯ 8,958 | π₯ 14,533,750 | π₯ 416.8 |
| 2^18 (262,144) | 95% | π₯ 8,957 | π₯ 11,637,000 | π₯ 417.3 |
| 2^24 (16,777,216) | 5% | π₯ 561,484 | π₯ 262,352,000 | π₯ 45,802 |
| 2^24 (16,777,216) | 50% | π₯ 585,361 | π₯ 2,088,303,000 | π₯ 45,800 |
| 2^24 (16,777,216) | 95% | π₯ 591,437 | π₯ 1,822,287,000 | π₯ 45,969 |
| Universe size | Fill density | hibitset (ns) | stdset (ns) | dynset (ns) |
|---|---|---|---|---|
| 2^12 (4,096) | 5% | π₯ 171.1 | π₯ 6,489 | π₯ 106.8 |
| 2^12 (4,096) | 50% | π₯ 1,050 | π₯ 89,691 | π₯ 744.1 |
| 2^12 (4,096) | 95% | π₯ 1,907 | π₯ 266,793 | π₯ 1,475 |
| 2^18 (262,144) | 5% | π₯ 11,503 | π₯ 1,134,759 | π₯ 5,147 |
| 2^18 (262,144) | 50% | π₯ 64,450 | π₯ 17,942,500 | π₯ 45,953 |
| 2^18 (262,144) | 95% | π₯ 129,105 | π₯ 38,129,500 | π₯ 87,837 |
| 2^24 (16,777,216) | 5% | π₯ 2,053,727 | π₯ 236,304,000 | π₯ 642,863 |
| 2^24 (16,777,216) | 50% | π₯ 8,438,500 | π₯ 6,531,067,000 | π₯ 6,579,800 |
| 2^24 (16,777,216) | 95% | π₯ 14,887,800 | π₯ 14,431,485,000 | π₯ 14,314,800 |
hibitset gains major query speedups while staying in a competitive range of dynset in memory usage.
| Universe size | Fill density | hibitset (bytes) | stdset (bytes) | dynset (bytes) |
|---|---|---|---|---|
| 2^12 (4,096) | 0% | π₯ 528 | π₯ 24 | π₯ 544 |
| 2^12 (4,096) | 0.1% | π₯ 528 | π₯ 184 | π₯ 544 |
| 2^12 (4,096) | 1% | π₯ 528 | π₯ 1,624 | π₯ 544 |
| 2^12 (4,096) | 5% | π₯ 528 | π₯ 8,184 | π₯ 544 |
| 2^12 (4,096) | 10% | π₯ 528 | π₯ 16,384 | π₯ 544 |
| 2^12 (4,096) | 25% | π₯ 528 | π₯ 40,984 | π₯ 544 |
| 2^12 (4,096) | 50% | π₯ 528 | π₯ 81,944 | π₯ 544 |
| 2^12 (4,096) | 75% | π₯ 528 | π₯ 122,904 | π₯ 544 |
| 2^12 (4,096) | 100% | π₯ 528 | π₯ 163,864 | π₯ 544 |
| 2^18 (262,144) | 0% | π₯ 33,296 | π₯ 24 | π₯ 32,800 |
| 2^18 (262,144) | 0.1% | π₯ 33,296 | π₯ 10,504 | π₯ 32,800 |
| 2^18 (262,144) | 1% | π₯ 33,296 | π₯ 104,864 | π₯ 32,800 |
| 2^18 (262,144) | 5% | π₯ 33,296 | π₯ 524,304 | π₯ 32,800 |
| 2^18 (262,144) | 10% | π₯ 33,296 | π₯ 1,048,584 | π₯ 32,800 |
| 2^18 (262,144) | 25% | π₯ 33,296 | π₯ 2,621,464 | π₯ 32,800 |
| 2^18 (262,144) | 50% | π₯ 33,296 | π₯ 5,242,904 | π₯ 32,800 |
| 2^18 (262,144) | 75% | π₯ 33,296 | π₯ 7,864,344 | π₯ 32,800 |
| 2^18 (262,144) | 100% | π₯ 33,296 | π₯ 10,485,784 | π₯ 32,800 |
| 2^24 (16,777,216) | 0% | π₯ 2,130,448 | π₯ 24 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 0.1% | π₯ 2,130,448 | π₯ 671,104 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 1% | π₯ 2,130,448 | π₯ 6,710,904 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 5% | π₯ 2,130,448 | π₯ 33,554,424 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 10% | π₯ 2,130,448 | π₯ 67,108,864 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 25% | π₯ 2,130,448 | π₯ 167,772,184 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 50% | π₯ 2,130,448 | π₯ 335,544,344 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 75% | π₯ 2,130,448 | π₯ 503,316,504 | π₯ 2,097,184 |
| 2^24 (16,777,216) | 100% | π₯ 2,130,448 | π₯ 671,088,664 | π₯ 2,097,184 |
include(FetchContent)
FetchContent_Declare(hibitset
GIT_REPOSITORY https://github.com/berkinarisoy/hibitset-cpp.git
GIT_TAG v0.1.0)
FetchContent_MakeAvailable(hibitset)
target_link_libraries(your_target PRIVATE hibitset::hibitset)You can vendor the header file include/hibitset/hibitset.hpp into your project and include it in your source files. No additional dependencies are required.
- C++20
- CMake 3.23 (or higher)
test |
accesses specific bit |
all, any, empty |
checks if all/any/none of the bits are set |
capacity |
returns the universe size |
count |
returns the number of set bits |
first, last |
returns the first/last set bit |
reset |
clears all bits |
set |
sets a specific bit to true |
clear |
sets a specific bit to false |
next_set |
returns the next set bit at or after a given index |
prev_set |
returns the previous set bit at or before a given index |
operator==, operator!= |
compares two bitsets for equality |
operator|=, operator&=, operator^= |
performs bitwise OR, AND, XOR in-place |
operator|, operator&, operator^ |
performs bitwise OR, AND, XOR and returns a new bitset |
npos |
returns a sentinel value for "not found" |
begin, end |
returns iterators to the first and one-past-the-last set bits (npos if none) |
cbegin, cend |
returns const iterators to the first and one-past-the-last set bits (npos if none) |
hibitset has been fuzz-tested with the std::set as an oracle, along with a set of unit tests that cover the API. The tests can be run by executing:
cmake -B build-debug -S .
cmake --build build-debug -j
ctest --test-dir "build-debug" --output-on-failureExample with U = 2^8, chunk_size = 4:
Layer 3: 1------------------------------------------------ ...
Layer 2: 1------------------ 1------------------ 0-------- ...
Layer 1: 1--- 0--- 0--- 0--- 1--- 0--- 1--- 0--- 0--- 0--- ...
Layer 0: 0010 0000 0000 0000 0011 0000 1111 0000 0000 0000 ...
In the real implementation each chunk is a uint64_t (64 bits) (the diagram above shrinks that to 4 bits per chunk for demonstration). The base layer (layer 0) holds the actual bits, with set bits at indices
- Layer 1's bit
$i$ is 1 iff any of layer 0's bits in chunk$i$ are set. - Layer 2's bit
$i$ is 1 iff any of layer 1's bits in chunk$i$ are set. - Layer 3's bit
$i$ is 1 iff any of layer 2's bits in chunk$i$ are set.
When we call next_set(2), the algorithm checks chunk 0 in layer 0 for a set bit at or after index 2. Since chunk 0's only set bit is index 1, it ascends to layer 1 looking past chunk 0's position, but bits 1β3 are all 0, so it ascends again to layer 2's bit 0, which only covers that same empty span. Moving to layer 2's bit 1, it finds a 1, descends into layer 1's bits 4β7, finds bit 4 set, descends once more into layer 0's chunk 4 (0011), and returns its lowest set bit, index 16. Instead of scanning every chunk in between, the algorithm skipped straight from chunk 0 to chunk 4 by climbing to the layer where that gap is summarized by a single 0.
For a real next_set/prev_set is