回顾bitmap
最近网上冲浪的时候看到有种数据结构叫roaring bitmap(咆哮位图),据说被许多的开源项目如Elasticsearch, Apache Druid, Apache Spark用到,很强很强。特地研究了一下,现将研究结果总结如下。
什么是bitmap?
逻辑上是一个set,物理上被叫做bitmap,实际上是整数数组。
众所周知,bitmap可被用来实现布隆过滤器。
怎样实现bitmap?
使用32bit(或者64bit)的整数数组。
一个32bit的整数表示一个大小(或称为基、cardinality)为32的集合,再用若干个32bit的整数,即可表示一个大小n的集合。
为什么要使用bitmap?
使用一个bitmap实现一个set,可以获得存储效率上的提升,与集合操作效率上的提升。使用bitmap,内存占用更小,集合常规操作如交、并等,可以用位运算实现,简洁高效,深受广大程序员喜欢。
传统bitmap有什么问题?
bitmap不是银弹,某些情况下不适用于bitmap。比如”稀疏“场景。当集合中存在大量的0,极少数的1时,若执意使用bitmap,会浪费存储空间,也会降低集合运算(需要访问大量无意义的0,时间退化到渐进线性)。
什么是roaring bitmap?
一种压缩位图实现,空间性能与时间性能更加
roaring bitmap解决了什么问题?
Run-length-encoded(RLE)bitmap(我将其翻译为长距离编码位图),对传统bitmap中的一连串1或者一连串0进行编码,已达到压缩的目的。想法挺好,但是带来的副作用是,元素的随机访问性能退化成线性时间,即判断一个元素是否在bitmap中,需要线性扫描已编码好的区块。
Roaring bitmap的提出,在RLE的基础上,解决了随机访问性能退化的问题。
怎样解决的?
Roaring bitmap的基本思想,是将传统的bitmap改为二段式索引,一级索引是一个容器数组,二级索引是存放数据的具体容器,可分为两类:bitmap container及array container。
举个例子,一个32bit的整数821697800,其16进制表示为 30FA1D08,高16位是一级索引,低16位是二级索引,如下图所示,高16位0x30FA在容器数组做了一次粗定位(使用二分搜索),低16位0x1D08 = 7432在容器(这里是一个bitmap container)内部做了二次定位(也使用二分搜索),最终将对应位置设为了1
另一个例子191037如下图所示,与第一个例子不同的是,191037存入的是一个array container。
Bitmap container vs. Array container
论文_Better bitmap performance with Roaring bitmaps_中指出,当整数个数(基)小于等于4096时,使用array container,否则使用bitmap。
为什么是4096?
网上有人说是为了节省存储空间,我不理解。
我认为是硬性约定,因为论文规定container最多只能存65536bit的数据。array container中存储4096个16bit整数的空间恰好等于65536bit,若整数个数大于4096个,将array container升级为bitmap container,才能存下更多的数据,同时不破坏约定。
算法实现
具体的算法实现如下,摘自论文_Better bitmap performance with Roaring bitmaps。代码仓库github.com/RoaringBitm… 有对应的golang实现供参考。
参考&扩展
- roaringbitmap.org/about/
- vikramoberoi.com/a-primer-on…
- github.com/RoaringBitm…
- Better bitmap performance with Roaring bitmaps: arxiv.org/pdf/1402.64…
- Consistently faster and smaller compressed bitmaps with Roaring: arxiv.org/pdf/1603.06…