一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
3 BitMap vs Roaring BitMap
可以分为以下两方面来进行对比:
-
内存上:
- bitmap比较适用于数据分布比较稠密的存储场景中,对于原始的Bitmap来说,若要存储uint32类型数据,这就需要2 ^ 32长度的bit数组 通过计算可以发现(2 ^ 32 / 8 bytes = 512MB), 一个普通的Bitmap需要耗费512MB的存储空间。如果我们只存储几个数据的话依然需要占用521M空间,这就有些浪费空间了,因此我们可以采用对位图进行压缩的RoaringBitMap,以此减少内存和提高效率。
-
性能上:
roaringbitmap除了比bitmap占用内存少之外,其并集和交集操作的速度也要比bitmap的快。原因归结为以下几点:
-
计算上的优化
对于roaringbitmap本质上是将大块的bitmap分成各个小块,其中每个小块在需要存储数据的时候才会存在。所以当进行交集或并集运算的时候,roaringbitmap只需要去计算存在的一些块而不需要像bitmap那样对整个大的块进行计算。如果块内非常稀疏,那么只需要对这些小整数列表进行集合的 AND、OR 运算,这样的话计算量还能继续减轻。这里既不是用空间换时间,也没有用时间换空间,而是用逻辑的复杂度同时换取了空间和时间。
同时在roaringbitmap中32位长的数据,被分割成高 16 位和低 16 位,高 16 位表示块偏移,低16位表示块内位置,单个块可以表达 64k 的位长,也就是 8K 字节。这样可以保证单个块都可以全部放入 L1 Cache,可以显著提升性能。
-
程序逻辑上的优化
(1)roaringbitmap维护了排好序的一级索引,以及有序的arraycontainer当进行交集操作的时候,只需要根据一级索引中对应的值来获取需要合并的容器,而不需要合并的容器则不需要对其进行操作直接过滤掉。
(2)当进行合并的arraycontainer中数据个数相差过大的时候采用基于二分查找的方法对arraycontainer求交集,避免不必要的线性合并花费的时间开销。
(3)roaingbitmap在做并集的时候同样根据一级索引只对相同的索引的容器进行合并操作,而索引不同的直接添加到新的roaringbitmap上即可,不需要遍历容器。
(4)roaringbitmap在合并容器的时候会先预测结果,生成对应的容器,避免不必要的容器转换操作。
-
4 应用场景
4.1 海量数据判断某个数是否存在?
举一个具体的例子,在go中一个int占用4个字节,如果我们有大量处理int的场景,比如一次处理10亿个int,那么大概需要消耗(10亿*4)/1024/1024/1024≈3.7个G左右的内存空间。而如果采用BitMap去存储这10亿个int,只需要消耗内存10亿Bit/8/1024/1024 ≈ 119MB,可见BitMap的存储优势,所以我们只需要遍历10个亿数字,映射到BitMap中,然后对于给出的数,直接判断指定的位上存在不存在即可。
4.2 使用位图法判断正整形数组是否存在重复
遍历一遍,存在之后设置成1,每次放之前先判断是否存在,如果存在,就代表该元素重复。
4.3 使用位图法进行元素不重复的正整形数组排序
遍历一遍,设置状态1,然后再次遍历,对状态等于1的进行输出,参考计数排序的原理。
4.4 在2.5亿个整数中找出不重复的正整数,注,内存不足以容纳这2.5亿个整数
解法1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)。
解法2:采用两个BitMap,即第一个Bitmap b1存储的是整数是否出现,接着,在之后的遍历先判断b1里面是否出现过,如果出现就设置第二个BitMap b2对应的位置也为1,最后取b1&(^b2)的结果,对应的就是仅仅在一个BitMap中出现过的元素,它就是不重复的整数。
解法3:分治+Hash取模,拆分成多个小文件,然后一个个文件读取,直到内存装的下,然后采用Hash+Count的方式判断即可。
该类问题的变形问题,如已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==12MBytes,这样,就用了小小的12M左右的内存表示了所有的8位数的电话)
4.5 倒排索引
我们在设计数据库表结构的时候,会有如下类似的表结构:
| id | Name | Sex | Phone |
|---|---|---|---|
| 1 | 张三 | 男 | 小米 |
| 2 | 李四 | 女 | 华为 |
| 3 | 王五 | 男 | 小米 |
| 4 | 周六 | 男 | 华为 |
| … | … | … | … |
在使用sql查询时,直接使用主键id查询对应的数据条目叫做主键索引,可以理解为通过key去找value。与之对应的是倒排索引,即通过关键字去查找主键id,比如我们想要统计所有使用小米手机的男性数量,需要如下的sql语句:
Select count(distinct Name) as 用户数 from table whare Sex = '男' and Phone = '小米' ;
倒排索引需要扫描表中所有数据,找到包含关键字的数据条目,再对多个用户求并集用distinct去重,上面语句看似没有任何问题,但是随着表维度以及表内数据越来越多(比如一个用户有几百上千个标签),sql性能很难满足实时返回。
这时候就可以考虑使用BitMap来维护倒排关系,给每个标签创建一个BitMap,然后存储包含此标签的id,如下所示:
| Sex | BitMap |
|---|---|
| 男 | 1,3,4… |
| 女 | 2,… |
| Phone | BitMap |
|---|---|
| 小米 | 1,3,… |
| 华为 | 2,4,… |
通过BitMap实现用户去重和查询统计,会非常方便:
如上所示,在求使用小米手机的男性用户时,只需要相『与』即可:
实际生产环境可以考虑这几个库:
5 总结
综上所述,本文主要介绍了BitMap以及Roaring BitMap的基本原理和应用案例,BitMap本质上是采用了bit位来存储元素状态,它适用于大规模数据,但数据状态又不是很多的情况,从而在特定场景下能够极大的节省存储空间,非常适合对海量数据的查找,判重,删除等问题的处理。