1. 概述
本教程将深入探讨 Roaring Bitmap 数据结构。我们将通过集合的基本操作示例来演示 Roaring Bitmap 的使用,并对比 Java 中 RoaringBitmap 与 BitSet 的性能表现。
2. Roaring Bitmap 简介
Roaring Bitmap 因其高性能和高压缩比,在数据分析、搜索引擎和大数据项目中广泛应用。其核心思想源于位图索引(bitmap indexes),这是一种高效表示数字数组的数据结构。可以将其视为压缩版的 Java *BitSet*。
Roaring Bitmap 的核心优势在于:既能高效压缩大整数集合,又能快速访问单个元素。其内部通过多种容器类型实现这一目标。
3. Roaring Bitmap 工作原理
Roaring Bitmap 是由多个不相交子集容器组成的无符号整数集合。每个子集通过 16 位键索引,可存储 2^16 范围内的值。这种设计允许将 32 位无符号整数存储为 Java short 类型,因为最坏情况下仅需 16 位即可表示单个 32 位值。容器大小的选择确保了即使最坏情况下,容器仍能适配现代 CPU 的 L1 缓存。
下图展示了 Roaring Bitmap 的结构:
整数的 16 个最高有效位作为桶(bucket)或块(chunk)键。每个数据块表示区间 (0 ≤ n < 2^16) 的值范围基础。若值范围内无数据,则不会创建对应块。
下图是包含不同数据的 Roaring Bitmap 示例:
- 第一块存储前 10 个 2 的倍数
- 第二块存储从 65536 开始的 100 个连续整数
- 第三块存储 131072 到 19660 之间的偶数
4. Roaring Bitmap 的容器类型
Roaring Bitmap 主要包含三种容器:数组容器、位图容器和行程容器。根据分区集合的特性,系统自动选择最优容器类型存储分区数据。
当向 Roaring Bitmap 添加数据时,内部会根据值是否在容器键覆盖范围内,决定创建新容器或修改现有容器。
4.1. 数组容器
数组容器不压缩数据,仅存储少量元素。其空间占用与数据量成正比:每个元素占 2 字节。
数组容器使用 short 数据类型,整数按升序存储。
初始容量为 4,最大元素数为 4096。数组容量动态变化,但当元素数超过 4096 时,Roaring Bitmap 会自动将其转换为位图容器。
以插入数字 131090 为例:
- 16 个最高有效位:
0000 0000 0000 0010
(作为键) - 16 个最低有效位:
0000 0000 0001 0010
(十进制值 18)
插入后的结构如下:
插入后,该键对应的数组容器基数为 5。
4.2. 位图容器
位图容器是位图的经典实现。
Roaring Bitmap 使用 long 数组存储位图数据。与动态扩展的数组容器不同,其数组容量恒定为 1024。位图容器无需查找位置,直接通过索引访问。
以插入数字 32786 为例:
- 16 个最高有效位:
0000 0000 0000 0000
- 剩余位:
1000 0000 0001 0010
(十进制值 32786)
该值表示位图容器中需设置的位索引。插入后的结构如下:
4.3. 行程容器
当位图区域存在大量连续值时,行程长度编码(RLE)是最佳容器选择。它使用 short 数组存储数据。
- 偶数索引值表示行程起点
- 奇数索引值表示行程长度
- 容器基数通过遍历整个行程数组计算
下图展示了连续整数序列的压缩过程:
压缩后仅存储 4 个值:
- 11 后跟 4 个连续递增值
- 27 后跟 2 个连续递增值
⚠️ 压缩效果取决于数据紧凑性:
- 100 个连续 short 可从 200 字节压缩到 4 字节
- 100 个分散 short 编码后从 200 字节膨胀到 400 字节
5. RoaringBitmap 的并集操作
在开始代码示例前,需在 pom.xml 中添加 RoaringBitmap 依赖:
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.38</version>
</dependency>
并集是首个测试操作。首先声明两个 RoaringBitmap 实例 A 和 B:
@Test
void givenTwoRoaringBitmap_whenUsingOr_thenWillGetSetsUnion() {
RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3, 4, 5, 6, 7, 8);
RoaringBitmap A = RoaringBitmap.bitmapOf(1, 2, 3, 4, 5);
RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
RoaringBitmap union = RoaringBitmap.or(A, B);
assertEquals(expected, union);
}
代码说明:
- 使用 bitmapOf() 静态工厂方法 创建实例
- 通过 or() 方法执行并集操作
- 底层执行位图逻辑 OR 运算,且线程安全
6. RoaringBitmap 的交集操作
交集是另一个常用操作。RoaringBitmap 中的交集实现同样简单直接:
@Test
void givenTwoRoaringBitmap_whenUsingAnd_thenWillGetSetsIntersection() {
RoaringBitmap expected = RoaringBitmap.bitmapOf(4, 5);
RoaringBitmap A = RoaringBitmap.bitmapOfRange(1, 6);
RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
RoaringBitmap intersection = RoaringBitmap.and(A, B);
assertEquals(expected, intersection);
}
关键点:
- 使用 bitmapOfRange() 创建范围集合
- 底层调用 add() 方法添加区间数据
- 接收两个 long 参数:下界(包含)和上界(不包含)
- 接收两个 int 参数的 add() 方法已废弃
- and() 方法执行交集操作
- 底层执行位图逻辑 AND 运算,线程安全
7. RoaringBitmap 的差集操作
差集(相对补集)操作通过 andNot() 方法实现:
@Test
void givenTwoRoaringBitmap_whenUsingAndNot_thenWillGetSetsDifference() {
RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3);
RoaringBitmap A = new RoaringBitmap();
A.add(1L, 6L);
RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
RoaringBitmap difference = RoaringBitmap.andNot(A, B);
assertEquals(expected, difference);
}
操作说明:
- 集合 A 通过空实例 + add() 方法创建
- andNot() 计算 A 与 B 的差集*
- 底层执行位图逻辑 ANDNOT 运算
- 只要输入位图不变,操作即线程安全
8. RoaringBitmap 的异或操作
异或(XOR)操作类似于对称差集,结果集排除两集合的公共元素:
@Test
void givenTwoRoaringBitmap_whenUsingXOR_thenWillGetSetsSymmetricDifference() {
RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3, 6, 7, 8);
RoaringBitmap A = RoaringBitmap.bitmapOfRange(1, 6);
RoaringBitmap B = RoaringBitmap.bitmapOfRange(4, 9);
RoaringBitmap xor = RoaringBitmap.xor(A, B);
assertEquals(expected, xor);
}
核心特性:
- xor() 方法执行位图逻辑 XOR 运算
- 线程安全操作
9. 与 BitSet 的性能对比
使用 JMH 对比 RoaringBitmap 和 Java BitSet 的性能。首先添加依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
9.1. 基准测试范围声明
@State(Scope.Thread)
class BitSetsBenchmark {
private RoaringBitmap rb1;
private BitSet bs1;
private RoaringBitmap rb2;
private BitSet bs2;
private final static int SIZE = 10_000_000;
}
关键配置:
- 声明两种类型的集合各两个
- 设置最大集合大小
SIZE = 10_000_000
- 使用 Scope.Thread 和默认吞吐量模式
- 为避免修改输入数据,每次操作生成新集合
- BitSet 测试中通过克隆保护输入数据
9.2. 基准测试数据初始化
@Setup
public void setup() {
rb1 = new RoaringBitmap();
bs1 = new BitSet(SIZE);
rb2 = new RoaringBitmap();
bs2 = new BitSet(SIZE);
for (int i = 0; i < SIZE / 2; i++) {
rb1.add(i);
bs1.set(i);
}
for (int i = SIZE / 2; i < SIZE; i++) {
rb2.add(i);
bs2.set(i);
}
}
数据分布:
- 集合 1(rb1/bs1):包含 0 到 SIZE/2-1 的值
- 集合 2(rb2/bs2):包含 SIZE/2 到 SIZE-1 的值
9.3. 基准测试用例
并集操作
@Benchmark
public RoaringBitmap roaringBitmapUnion() {
return RoaringBitmap.or(rb1, rb2);
}
@Benchmark
public BitSet bitSetUnion() {
BitSet result = (BitSet) bs1.clone();
result.or(bs2);
return result;
}
交集操作
@Benchmark
public RoaringBitmap roaringBitmapIntersection() {
return RoaringBitmap.and(rb1, rb2);
}
@Benchmark
public BitSet bitSetIntersection() {
BitSet result = (BitSet) bs1.clone();
result.and(bs2);
return result;
}
差集操作
@Benchmark
public RoaringBitmap roaringBitmapDifference() {
return RoaringBitmap.andNot(rb1, rb2);
}
@Benchmark
public BitSet bitSetDifference() {
BitSet result = (BitSet) bs1.clone();
result.andNot(bs2);
return result;
}
异或操作
@Benchmark
public RoaringBitmap roaringBitmapXOR() {
return RoaringBitmap.xor(rb1, rb2);
}
@Benchmark
public BitSet bitSetXOR() {
BitSet result = (BitSet) bs1.clone();
result.xor(bs2);
return result;
}
9.4. 基准测试结果
Benchmark Mode Cnt Score Error Units
BitSetsBenchmark.bitSetDifference thrpt 25 3890.694 ± 313.808 ops/s
BitSetsBenchmark.bitSetIntersection thrpt 25 3542.387 ± 296.007 ops/s
BitSetsBenchmark.bitSetUnion thrpt 25 3012.666 ± 503.821 ops/s
BitSetsBenchmark.bitSetXOR thrpt 25 2872.402 ± 348.099 ops/s
BitSetsBenchmark.roaringBitmapDifference thrpt 25 12930.064 ± 527.289 ops/s
BitSetsBenchmark.roaringBitmapIntersection thrpt 25 824167.502 ± 30176.431 ops/s
BitSetsBenchmark.roaringBitmapUnion thrpt 25 6287.477 ± 250.657 ops/s
BitSetsBenchmark.roaringBitmapXOR thrpt 25 6060.993 ± 607.562 ops/s
结果分析:
- ✅ RoaringBitmap 在所有操作中均优于 BitSet
- ⚠️ 但需注意适用场景:
- 小数据集时使用压缩位图可能浪费资源
- 当解压 BitSet 不会增加内存占用时,压缩位图可能不适用
- 若无需压缩,BitSet 提供更极致的速度
10. 总结
本文深入研究了 Roaring Bitmap 数据结构,演示了 RoaringBitmap 的核心操作,并通过性能测试验证了其相对 BitSet 的优势。测试结果表明,在多数场景下 RoaringBitmap 性能显著优于传统 BitSet。
完整示例代码可在 GitHub 获取。