What Happens When Bitmap Boundaries Listen to the Data?
What if bitmap container boundaries adapted to data density instead of falling at fixed intervals? We benchmarked 14 operations at three scales to find out. The results surprised us in both directions.
Research in progress
The data and performance claims on this page reflect ongoing research, implementation, and experimentation. They may change as benchmarks are rerun and formal papers are finalized.
We benchmarked Diatom Bitmap against Roaring Bitmap across 14 operations at three scales (1K, 10K, 100K values). Diatom wins decisively on aligned set operations at scale. Roaring wins on point queries and construction. This post presents every number.
In our earlier post, we described the limitations of fixed-boundary bitmap indexing: uniform 2^16 partitioning that ignores data topology, containers that don't know their own density, and no mechanism for thermal adaptation. We asked whether a density-aware bitmap could do better.
This post is the answer. We ran Criterion benchmarks on every core operation, comparing Diatom Bitmap against roaring-rs (the canonical Rust implementation of Roaring Bitmap). The results are not uniformly favorable. That's why they're worth reporting.
Set Operations at Scale
At 100K values with aligned boundaries, Diatom Bitmap outperforms Roaring on every set operation. AND intersection is 7.4x faster. Cardinality-only AND is 19.3x faster.
Set operations are the hot path for bitmap indexes in analytics engines. Every query in Apache Druid, Apache Pinot, or ClickHouse that combines filter predicates runs AND, OR, or ANDNOT across bitmap indexes.
| Operation | 1K | 10K | 100K |
|---|---|---|---|
| AND | ~parity | 1.09x faster | 7.4x faster |
| OR | 3.2x slower | ~parity | 2.7x faster |
| ANDNOT | 1.2x slower | 1.05x faster | 6.0x faster |
| XOR | 1.1x slower | 1.22x faster | 2.9x faster |
| AND cardinality | 2.7x slower | ~parity | 19.3x faster |
The pattern is consistent: Diatom has fixed overhead at small sizes (1K), reaches parity around 10K, and pulls ahead at 100K. The gap widens with scale.
At 100K, aligned AND is 232 ns versus 1,724 ns, or 7.4x faster.
At 100K, aligned OR is 732 ns versus 1,984 ns, or 2.7x faster.
At 100K, aligned AND cardinality is 62 ns versus 1,202 ns, or 19.3x faster.
The absolute numbers at 100K:
- AND: Diatom 232 ns vs Roaring 1,724 ns
- OR: Diatom 732 ns vs Roaring 1,984 ns
- ANDNOT: Diatom 334 ns vs Roaring 2,003 ns
- XOR: Diatom 686 ns vs Roaring 2,002 ns
- AND cardinality: Diatom 62 ns vs Roaring 1,202 ns
The 19.3x result on cardinality-only AND deserves explanation. Three factors compound:
- Cached popcount. Diatom caches cardinality at the bitmap level. Roaring recalculates container cardinalities during intersection.
- Empty-container short-circuit. Aligned Diatom containers that are empty skip entirely. No iteration, no word operations, no popcount. At 100K with density-derived boundaries, most containers are empty after intersection.
- Fewer containers. Density-derived boundaries coalesce sequential data into fewer, larger containers than Roaring's fixed 2^16 partitioning. Fewer containers means fewer loop iterations at the set-operation level.
These results require aligned boundaries (both bitmaps sharing the same boundary structure). We address what happens without alignment in a later section.
Where Diatom Also Wins
Beyond set operations, Diatom stays faster on cardinality queries (1.9-2.7x) and serialization (1.8-8.5x). Iteration is now closer to parity: faster at 1K and 100K, slightly slower at 10K.
Cardinality is O(1) in Diatom (a cached value at the bitmap level) versus O(containers) in Roaring. The result stays in a tight sub-nanosecond band across the tested sizes:
At 100K, cached cardinality is 0.47 ns versus 1.25 ns, or 2.7x faster.
| Size | Diatom | Roaring | Ratio |
|---|---|---|---|
| 1K | 0.46 ns | 0.87 ns | 1.9x faster |
| 10K | 0.48 ns | 0.93 ns | 1.9x faster |
| 100K | 0.47 ns | 1.25 ns | 2.7x faster |
Iteration is no longer a decisive headline win, but it remains competitive. Bitmap containers iterate over u64 words using trailing-zeros extraction. Array containers iterate sorted u16 slices. In the current implementation that is enough to edge out Roaring at 1K and 100K, but not at 10K:
At 100K, iteration is 317,377 ns versus 357,463 ns, or 1.1x faster.
| Size | Diatom | Roaring | Ratio |
|---|---|---|---|
| 1K | 2.8 us | 3.5 us | 1.3x faster |
| 10K | 38.0 us | 34.2 us | 1.1x slower |
| 100K | 317.4 us | 357.5 us | 1.1x faster |
Serialization uses a compact binary format (magic: DBTM). At 10K, Diatom serializes 8.5x faster than Roaring:
At 10K, serialization is 211 ns versus 1,792 ns, or 8.5x faster.
| Size | Diatom | Roaring | Ratio |
|---|---|---|---|
| 1K | 701 ns | 1,256 ns | 1.8x faster |
| 10K | 211 ns | 1,792 ns | 8.5x faster |
Where Diatom Loses
Point queries (contains) are still slower than Roaring, but the corrected 100K story is much narrower than the old draft implied. Construction (from_sorted) is still 3.7-5.8x slower. Insert stays slower on both sequential and sparse workloads. We publish losses alongside wins.
contains() hit path at 100K: 2.2x slower after the integrity fix. This remains the clearest standalone regression, but it is no longer the catastrophic 7.6x story the old draft carried. A March 20, 2026 investigation found that the earlier 100K result was being inflated by a boundary-range correctness bug. After fixing that path, the standalone contains() penalty remains real, but it is now closer to the cost you would expect from explicit boundary lookup plus in-container search.
On present values at 100K, Diatom takes 463,569 ns versus Roaring's 208,302 ns, or 2.2x slower.
| Size | Diatom | Roaring | Ratio |
|---|---|---|---|
| 1K | 11.7 us | 8.9 us | 1.3x slower |
| 10K | 35.2 us | 12.1 us | 2.9x slower |
| 100K | 463.6 us | 208.3 us | 2.2x slower |
The corrected 100K result still says the same qualitative thing: Roaring is better for standalone point lookups. It no longer supports the stronger claim that Diatom falls off a cliff at scale on this path.
Miss-path contains() is milder at 100K: 228,482 ns versus 138,958 ns, or 1.6x slower.
from_sorted(): 3.7-5.8x slower. Constructing a Diatom Bitmap from sorted values runs a density histogram followed by valley detection to place boundaries. Roaring just distributes values into fixed 2^16 containers. The boundary placement algorithm is the cost of adaptivity. It's O(n) but with a higher constant factor.
At 100K, bulk construction is 2,495,236 ns versus 429,623 ns, or 5.8x slower.
| Size | Diatom | Roaring | Ratio |
|---|---|---|---|
| 1K | 15.4 us | 4.2 us | 3.7x slower |
| 10K | 183.8 us | 44.5 us | 4.1x slower |
| 100K | 2,495.2 us | 429.6 us | 5.8x slower |
Insert: 1.8-2.3x slower. Each insert must locate the correct container via boundary lookup and potentially update density metadata. The overhead is constant per operation and doesn't grow disproportionately with scale.
These are real costs. Whether they are acceptable depends on your workload. If your hot path is set operations on aligned bitmap families, Diatom has a real case. If your hot path is one-off point lookups (contains) or bulk construction, Roaring is the better choice today.
Deserialization is no longer a weakness. Diatom still serializes 8.5x faster than Roaring at 10K, and after fixing the readback path it now deserializes 2.0x faster too: 412 ns versus 819 ns. The earlier asymmetry turned out to be partly a correctness bug in disguise: bitmap payloads were being rebuilt bit-by-bit on load, and compressed containers were not round-tripping correctly.
The 1K overhead story. At 1K values, Diatom still pays visible overhead on point queries and several set operations, but it is already competitive on cardinality, iteration, and serialization. The current picture is less “slow on everything small” and more “mixed at 1K, compelling only once the workload reaches aligned set operations or larger scales.” Diatom is still not designed for tiny standalone bitmaps.
The Scaling Story
Scale helps Diatom most when boundaries are aligned and the workload is dominated by set work. It does not erase the standalone costs.
Roaring uses fixed 2^16 boundaries. Every 65,536 consecutive values get one container. The container count scales linearly with value range, regardless of data distribution. Dense regions and sparse regions get the same treatment.
Diatom uses density-derived boundaries. The boundary placement algorithm runs a histogram over the value space and detects valleys in the density distribution. Dense regions get fewer, larger containers. Sparse regions get many small containers or none at all. At 100K sequential values, Diatom coalesces data into fewer containers than Roaring, with each container holding more values. Fewer containers means fewer loop iterations in set operations, fewer cache misses, and fewer function calls.
What scales is not a blanket “better bitmap” claim. What scales is the aligned set-operation path. At 1K, both approaches produce a handful of containers and Diatom's fixed overhead dominates. At 100K, aligned set work benefits from fewer effective containers and cached metadata. But standalone membership and construction still lose, and fixed-floor remains the honest standalone baseline inside Diatom itself.
The alignment requirement. These set operation numbers assume aligned boundaries, meaning both bitmaps share the same boundary structure. That is not a footnote. It is the current wedge.
Bitmaps in the same analytical domain should be built as a family and keep aligned morphology together. The current Symbiosis path supports shared-family construction, aligned operations, partner warming, and coordinated merge suppression. It does not yet justify a broad “adaptive wins everywhere” claim, but it does identify where Diatom is meaningfully different from a standalone fixed-boundary bitmap.
That is why the Diatom story has narrowed. The most credible use is coordinated bitmap families, not arbitrary standalone bitmaps dropped into Roaring's home turf.
What's Next
The next stage for Diatom is narrower than the first draft claimed: keep the truthful core, improve coordinated family maintenance, and prove whether shared morphology creates a decision-changing wedge.
We now know more precisely what the benchmark suite does and does not support. Diatom is not yet a general Roaring replacement. It is a more specific research primitive: adaptive morphology with a fixed-floor baseline and a plausible family-level wedge when related bitmaps share structure.
That is still worth studying. It is simply a narrower and more honest claim than the original draft.
Related Posts
Nacre Array Benchmarks: The Numbers
We promised the full benchmark numbers. Here they are: every operation, every scale, wins and losses. Vec is still faster at some things. Nacre Array is faster at others. The crossover points tell the real story.
Nacre Array: What If Your Vec Had a Spine?
Mother-of-pearl is 3,000 times tougher than the crystals it's made of. The secret is layered organization with flexible mortar between rigid segments. We built an array that works the same way.
What Happens When Your Index Navigates by Smell?
Echo Field just ran its first away-field benchmark on 18,840 real arXiv sections. 100% recall parity with brute-force, 18x faster at 500K vectors, and churn-stable under continuous mutation. Here's what we found.
What If Your Bitmap Knew Its Own Density?
Bitmap indexes power analytics queries across billions of rows. But the dominant implementation treats all data the same: fixed boundaries, uniform containers, no adaptation. What if the bitmap could see its own density distribution?
Why Nature Builds Better Arrays
Every system that stores sequential data eventually hits the same wall: cold data costs the same as hot, inserts require shifting everything, and there's no way to split without copying. What if the structure itself knew the difference?
Why Your Data Structure Doesn't Have a Metabolism
Data structures are benchmarked at birth, compared as static objects, and optimized for a single moment in time. But biological systems invest in costly metabolic machinery that pays off across a lifecycle. What if the overhead isn't the problem, but the investment?