有朋友提示可以使用Roaring Bitmap,咱们今天就看看什么是Roaring Bitmap。
什么是 Bitmap?
Bitmap是用于存储整数集的位数组。
它们的工作原理是当整数N位于集合中时设置第N位,如图:
什么是 Bitmap?
通过这种方式存储整数集,在进行数据操作时,可以使用CPU指令中的按位与和按位或指令来计算集合的交集和并集,CPU指令都是极快的。
集合交集和并集运行效率,对于许多搜索和数据库应用程序至关重要。搜索和数据库索引中存在各种操作,归根结底就是拥有两组整数,需要快速对它们进行交集或并集。
以反向搜索索引为例:
- 您已索引了数十亿个文档。每个文档都有一个整数 ID。
- 索引将术语映射到它们出现的一组文档。例如,术语pigeon出现在具有以下 ID 的文档中:{2, 345, 2034, ...}。
- 跨术语搜索的查询使用集合运算。为了解决类似这样的搜索查询,carrier AND pigeon您需要包含pigeon的文档集和包含carrier的文档集的交集。
- 按位运算可以快速执行这些集合运算。如果将文档 ID 集合表示为Bitmap,则上述查询就是按位与。
什么是Roaring Bitmap
在https://roaringbitmap.org/中的介绍简洁明了:
Roaring bitmaps are compressed bitmaps. They can be hundreds of times faster.
Bitmap和Roaring Bitmap都为整数提供了一组数据结构,可以插入整数、检查整数的存在以及获取两组整数的交集和并集。
Roaring Bitmap在补习生集合操作性能的前提下,比Bitmap具有更好的压缩效果。
roaringbitmap.org网站列出了一系列OLAP数据库和搜索系统,这些系统内部都使用了Roaring Bitmap。这些系统的详细点是:
- 需要存储大量整数
- 尽可能少的内存
- 执行快速设置
- ……
从侧面反映了Roaring Bitmap的优势。
Roaring Bitmap 解决了什么问题?
当集合稀疏时,Bitmap的压缩效果较差。
假设您有一个空的Bitmap,向其中添加整数8,000,000。将发生以下情况:
首先需要分配1,000,000字节。
然后设置第8,000,000位。
Bitmap的问题
这有什么不好吗?
很明显,集合中只有1个整数,整数占用4个字节,但是Bitmap已经分配了1兆,整整多了6个数量级。
妥妥的空间黑洞。
Roaring Bitmap就是为了解决这个问题的。
Roaring Bitmap 的工作原理
Roaring Bitmap是一组无符号整数,由不相交子集的容器组成。
每个子集都有一个16位的索引,可以保存大小为2^16范围内的值。
容器大小的选择还确保在最坏情况下,容器仍然适合现代CPU的L1缓存。
下图展示了Roaring Bitmap结构:
图片
我们的整数的高16位是索引(或者叫做存储桶或块键)。每个数据块代表间隔内值范围的基数(0<= n < 2^16)。此外,如果值范围内没有数据,则不会创建块。
下图是具有不同数据的Roaring Bitmap的示例:
图片
在第一个块中,我们存储了 2 的前 10 个倍数。此外,在第二个块中,我们有从 65536 开始的 100 个连续整数。图像中的最后一个块有 131072 到 19660 之间的偶数。
Roaring Bitmap 的容器
Roaring Bitmap 中有三种主要类型的容器 - 数组容器(Array Container)、Bitmap容器(Bitmap Container)和运行容器(Run Container)。
根据分区集的特征,数组容器、Bitmap容器和运行容器是保存分区数据的容器的实现。
当我们将数据添加到Roaring Bitmap时,它会在内部根据值是否适合容器键所涵盖的范围来决定是否创建新容器或更改现有容器。
数组容器
数组容器不压缩数据,只能保存少量数据,其占用的空间与保存的数据量成正比,每个数据占两个字节。
数组容器直接采用数组来存储低16位数据,没有采用任何数据压缩算法,适合存储比较稀疏的数据,在Java中,使用short数组来存储,并且占用的内存空间大小和数据量成线性关系。由于short为2字节,因此n个数据为2n字节。
数组容器的初始容量为4,最大数据量为4096,数组容量是动态变化的,但是当元素数超过4096时,Roaring Bitmap内部会将数组容器转换为Bitmap容器。4096 * 2b = 8kb,Bitmap容器空间也是8kb。
数组容器采用二分查找定位有序数组中的元素,因此时间复杂度为O(logN)。
让我们看一个在Roaring Bitmap中将数据插入数组容器的示例。
插入数字131090,高16位是 0000 0000 0000 0010,作为一级索引,低16位是 0000 0000 0001 0010,作为存储数据,低16位转换为十进制基数时,值为18。
现在,插入数据后,这是我们的Roaring Bitmap结构:
图片
Bitmap容器
Bitmap容器采用BitMap的原理,就是一个没有经过压缩处理的普通BitMap,适合存储比较稠密的数据,在Java中使用Long数组存储低16位数据,每一个bit位表示一个数字。由于每个container需要存储2^16 = 65536个数据,如果通过BitMap进行存储的话,需要使用2^16个bit进行存储,即8kb的数据空间。
Bitmap采用位运算,时间复杂度为O(1)。
为了观察它的工作原理,我们将使用一个简单的示例。我们将数字 32786 插入Roaring Bitmap中。高16位是 0000 0000 0000 0000 作为一级索引,低16位是1000 0000 0001 0010,对应的数据大于4096,需要使用Bitmap容器,对应的结果为:
图片
运行容器
运行容器采用行程长度编码(Run-Length Encoding,RLE)进行压缩,适合存储大量连续数据。Java中使用short数组进行存储。
连续bit位程度越高的话越节省存储空间,最佳场景下(65536个数据全为1)只需要存储4字节。最差场景为所有数据都不连续,所有存储数据位置为奇数或者偶数,这种场景需要存储128kb。
运行容器采用二分查找算法定位元素,因此时间复杂度为O(logN)。
偶数索引处的值表示运行的开始,奇数索引处的值表示这些运行的长度。容器的基数是通过遍历整个运行数组来计算的。
例如,下图展示了一个包含连续整数序列的容器。然后,在 RLE 执行之后,容器只有四个值:
图片
这些值表示为 11 后跟四个连续增加的值和 27 后跟两个连续增加的值。
这种压缩算法的工作原理取决于数据的紧凑程度或连续程度。如果我们有100个数,它们全都排成一行,它可以将它们从200字节压缩到4字节,但如果它们全都位于不同位置,则编码后会从200字节变为400字节。
文末总结
今天介绍了Roaring Bitmap,有用的知识又增加了。我们可以不用,但是不能知道。知道了,需要的时候就可以直接用了。
下次我们看下Java中如何使用Roaring Bitmap。