Roaring Bitmap
A Roaring bitmap is a hybrid compressed bitmap index that partitions 32-bit integers into 65,536-element chunks stored adaptively as sorted arrays, uncompressed bitsets, or run-length intervals.
Roaring bitmap is a hybrid compressed bitmap data structure within computation and algorithms designed to represent and manipulate sets of 32-bit or 64-bit unsigned integers with high memory efficiency and hardware-accelerated Single Instruction, Multiple Data (SIMD) throughput. Introduced by Samy Chambi, Daniel Lemire, Owen Kaser, and Robert Godin in 2016 (and extended with run-length containers by Lemire, Ssi-Yan-Kai, and Kaser in 2016), Roaring bitmaps overcome the cpu-bound decoding bottlenecks of traditional word-aligned run-length compressed bitmaps such as Word-Aligned Hybrid (WAH), Enhanced Word-Aligned Hybrid (EWAH), and Concise.
Instead of encoding an entire universe of 232 integers as a single monolithic bitstream of variable-length run words, a Roaring bitmap partitions the 32-bit integer space [0, 232 โ 1] into at most 216 = 65,536 contiguous chunks of 216 = 65,536 integers each, indexed by the most significant 16 bits of each integer. Within each chunk, the least significant 16 bits of the present integers are stored in one of three specialized containers chosen dynamically according to local density and contiguity: a packed sorted array of 16-bit integers (for sparse chunks with โค 4,096 elements), a fixed 8-kilobyte uncompressed bitset (for dense chunks with > 4,096 elements), or a run-length encoded (RLE) interval array (for contiguous runs). Today, Roaring bitmaps serve as the standard inverted-index and columnar bitmap engine in Apache Lucene, Elasticsearch, Apache Spark, ClickHouse, Apache Druid, and DuckDB.
Two-Level Chunk Partitioning and Adaptive Containers
High-16 / Low-16 Bit Decomposition
Every 32-bit unsigned integer x โ [0, 232 โ 1] is split by bit-shifting into two 16-bit unsigned words:
- Chunk Key (Most Significant 16 Bits): k = โx / 216โ = x >> 16 โ [0, 65,535].
- Container Offset (Least Significant 16 Bits): v = x mod 216 = x & 0xFFFF โ [0, 65,535].
A top-level index directory maintains a sorted array of active 16-bit chunk keys keys[0 .. m โ 1] alongside a parallel array of pointers to their corresponding containers containers[0 .. m โ 1]. Because empty chunks are omitted from the top-level directory, sparse sets spanning a large integer universe allocate zero memory for empty 65,536-integer regions.
The Three Container Formats and the 4,096-Element Threshold
Within each active 16-bit chunk, the set of 16-bit offsets Sk โ {0, 1, โฆ, 65,535} of cardinality c = |Sk| is stored using whichever of three container representations minimizes memory footprint while supporting random access:
- Array Container (Sparse: c โค 4,096): Stores the c 16-bit unsigned integers (16-bit unsigned integer) in a contiguous sorted dynamic array, plus a 16-bit cardinality counter. Each element consumes 2 bytes (16 bits), so the container size grows linearly as 2c bytes.
- Bitmap Container (Dense: c > 4,096): Stores a fixed-size uncompressed bit array of 216 = 65,536 bits, packed into 1,024 64-bit machine words (1,024 64-bit words), occupying 8,192 bytes (8 KB) regardless of how many bits are set, alongside a precomputed cardinality counter.
- Run Container (Contiguous Runs): Stores a sorted array of (start, length - 1) pairs of 16-bit integers ((16-bit start, 16-bit length)), where each 4-byte pair represents a contiguous closed interval [start, start + length - 1] of set bits. If a chunk contains r contiguous runs, the run container occupies 4r bytes.
Why is the crossover threshold between an Array Container and a Bitmap Container fixed at c = 4,096? At c = 4,096 elements, a sorted array of 16-bit integers occupies 4,096 ร 2 bytes = 8,192 bytes (8 KB)โthe identical byte size of a 65,536-bit uncompressed Bitmap Container. Below 4,096 elements, the sorted array uses strictly less than 8 KB; above 4,096 elements, the fixed 8 KB bitset uses less memory than a sorted array and fits completely inside the 32 KB L1 data cache of a modern CPU core. When inserting the 4,097-th element into an Array Container, it converts in place into a Bitmap Container; when deleting down to 4,096 elements, it converts back into an Array Container.
| Container Type | Cardinality / Structure Regime | Memory Footprint | Membership Lookup Contains(v) | Primary Hardware Acceleration |
|---|---|---|---|---|
| Array Container | Sparse (c โค 4,096 elements) | 2c bytes (โค 8 KB) | O(log c) binary search | SIMD galloping & shuffle merge (AVX2/AVX-512) |
| Bitmap Container | Dense (c > 4,096 elements) | Fixed 8,192 bytes (8 KB) | O(1) bit test (word[v>>6]) | 64-bit / 256-bit / 512-bit bitwise AND/OR/POPCNT |
| Run Container | r contiguous intervals (4r < min(2c, 8192)) | 4r bytes | O(log r) interval search | Fast interval sweep without unpacking individual bits |
Container-Pair Set Algebra and SIMD Vectorization
Pairwise Intersection, Union, and Difference Dispatch
To compute the intersection (A โฉ B), union (A โช B), or symmetric difference (A โ B) of two Roaring bitmaps, the top-level algorithm walks the two sorted 16-bit chunk-key arrays A.keys and B.keys via a two-pointer merge (or skewed galloping binary search when one bitmap has far fewer chunks than the other). Whenever both bitmaps contain a chunk with the same 16-bit key k, a specialized pairwise kernel is dispatched based on the types of the two containers:
- Bitmap โฉ Bitmap: Executes 1,024 64-bit bitwise AND operations (or 128 512-bit AVX-512 vpandq instructions) across the two 8 KB arrays while accumulating the new cardinality via hardware population-count instructions (POPCNT / vpopcntq) in roughly 150โ300 CPU clock cycles. If the resulting cardinality is > 4,096, the result remains a Bitmap Container; if โค 4,096, the set bits are extracted into an Array Container using trailing-zero instructions (TZCNT / ctzll).
- Array โฉ Bitmap: Iterates through the c โค 4,096 integers in the sparse Array Container and tests each integer against the Bitmap Container in O(1) time per element ((bitmap[v >> 6] >> (v & 63)) & 1), outputting matching integers directly into a result Array Container in O(c) branchless operationsโnever scanning the 8 KB bitmap.
- Array โฉ Array: When the two arrays have similar lengths, executes a SIMD-accelerated two-pointer merge comparing 8 or 16 16-bit unsigned integer elements per vector register; when one array is much smaller than the other (c1 ยท 64 < c2), executes a skewed galloping (doubling-stride) search in O(c1 log(c2 / c1)) time.
Comparison With WAH, EWAH, and Probabilistic Filters
In classical Word-Aligned Hybrid (WAH) and EWAH bitmaps, run-length fill words (runs of all-0 or all-1 31-bit or 63-bit words) are interleaved sequentially with literal bit words. Because run lengths vary dynamically, WAH and EWAH cannot perform O(1) random access to test whether integer x is set without scanning from the beginning of the compressed stream, and intersecting a 3-element sparse set with a 10-million-element dense set still requires decoding the dense stream. By isolating every 65,536-integer block into an independently addressable container and handling Array โฉ Bitmap in O(csparse) probes, Roaring bitmaps achieve 10ร to 100ร speedups over WAH/EWAH on skewed analytical queries while complementing probabilistic structures such as Bloom filters whenever zero false positives and lossless set cardinality are required.
64-Bit Extension (Roaring64Map) and Database Applications
Extending Roaring to 64-Bit Integers
For applications indexing 64-bit document identifiers, row offsets, or genomic coordinates (Roaring64Map), the high 32 bits of each 64-bit integer (x >> 32) are indexed using a cache-conscious B-tree or adaptive radix tree (ART), where each leaf points to a standard 32-bit Roaring bitmap storing the low 32 bits (x & 0xFFFFFFFF).
Columnar OLAP Databases, Inverted Search Indexes, and Genomics
In full-text search engines (Apache Lucene, Elasticsearch, Tantivy), posting lists of document IDs that exceed a frequency threshold are cached as Roaring bitmaps so multi-term Boolean filter clauses (status:active AND region:US AND NOT archived:true) execute as L1-cache-resident container operations. In columnar OLAP databases (ClickHouse, Apache Druid, Pinot, StarRocks), Roaring bitmaps power lossless distinct-count (COUNT(DISTINCT user)) sketches and bitmap indexes for sub-second funnel and cohort retention queries, while in bioinformatics, colored de Bruijn graph indices (such as Bifrost) use Roaring bitmaps to map each genomic k-mer to the compressed set of reference genomes in which it occurs.
Frequently Asked Questions
Why does Roaring use 16-bit chunks (65,536 integers) instead of 8-bit or 24-bit chunks?
Choosing 216 = 65,536 integers per chunk is an architectural sweet spot for modern CPU caches and integer widths: first, intra-chunk offsets fit cleanly in a native 16-bit unsigned integer (16-bit unsigned integer, 2 bytes per element in Array Containers); second, an uncompressed bitset of 216 bits occupies 8,192 bytes (8 KB), so two Bitmap Containers plus an output buffer (24 KB total) fit comfortably inside a single 32 KB or 48 KB L1 data cache during SIMD pairwise intersections.
How does a Roaring bitmap extract set integers from a Bitmap Container without testing all 65,536 bits?
Instead of looping over all 65,536 individual bit positions, Roaring iterates over the 1,024 64-bit words w = bitmap[k]. While w != 0, it extracts the index of the lowest set bit in a single CPU cycle using the hardware trailing-zero count instruction (hardware count-trailing-zeros on w / TZCNT), emits 64 * k + tz, and clears the lowest set bit via the bit-manipulation idiom w &= (w - 1) (BLSR). Consequently, iterating over all set bits in a Bitmap Container with c elements takes O(1024 + c) instructions rather than 65,536 loop iterations.
When should runOptimize() be called to convert Array or Bitmap Containers into Run Containers?
By default, dynamic single-element insertions create Array and Bitmap Containers to keep insertion latency at O(1) or O(log c). Once a bitmap is constructed (or before serializing it to disk or network storage), calling runOptimize() scans each container in O(1) time (counting bit transitions w ^ (w << 1) via POPCNT in Bitmap Containers) and converts any container whose run-encoded size 4r is smaller than both 2c and 8,192 bytes into a Run Container.
References
- Chambi, S., Lemire, D., Kaser, O., & Godin, R. (2016). "Better bitmap performance with Roaring bitmaps." Software: Practice and Experience, 46(5), 709โ719. https://doi.org/10.1002/spe.2325
- Lemire, D., Ssi-Yan-Kai, G., & Kaser, O. (2016). "Consistently faster and smaller compressed bitmaps with Roaring." Software: Practice and Experience, 46(11), 1547โ1569. https://doi.org/10.1002/spe.2402
- Lemire, D., Kaser, O., Kurz, N., Deri, L., O'Hara, C., Saint-Jacques, F., & Ssi-Yan-Kai, G. (2018). "Roaring bitmaps: Implementation of an optimized software library." Software: Practice and Experience, 48(4), 867โ895. https://doi.org/10.1002/spe.2560
- Wu, K., Otoo, E. J., & Shoshani, A. (2006). "Optimizing bitmap indices with efficient compression." ACM Transactions on Database Systems, 31(1), 1โ38. https://doi.org/10.1145/1132863.1132864