三种bitmap
| 分类 | 支持的存储类型 | 问题 | 常用实现库 |
|---|---|---|---|
| 普通bitmap( Java Bitset) | 支持Int 32位(4字节)类型 | - 内存占用较大,例如要存储int类型的数据,假如数据稀疏,只存储几个数,依然需要占用512M = 2^32(int的个数)/8/1024/1024 内存空间。 输入只支持 Int 类型。 | Java Bitset |
| RoaringBitmap( 压缩 bitmap) | 支持Int 32位(4字节)类型 | Roaring Bitmap库 | |
| Roaring64map(64位bitmap,更优) | 支持Bigint 64位(4字节)类型 | CRoaring库 |
为了优化内存占用,有了RoaringBitmap和Roaring64Map
CRoaring库同时支持32位和64位的整型集合存储。其中RoaringBitmap主要处理32位,Roaring64Map则利用RoaringBitmap实现了64位支持。
RoaringBitmap、Roaring64map和字典编码
RoaringBitmap
RoaringBitmap 的实现如下图所示,
- 第一层称之为 Chunk(高 16 位),如果该取值范围内没有数据则 Chunk 不会创建
- 第二层称之为 Container(低 16 位),会依据数据分布进行创建(Container内的值实际是区间内的offset)
RoaringBitmap Container有三种: Array Container ,Run Container和 Bitmap Container。
Array Container 存放稀疏的数据,Bitmap Container 存放稠密的数据。结合实现,若一个 Container 里面的 Integer 数量小于 4096,就用 Short 类型的有序数组来存储值。若大于 4096,就用 Bitmap 来存储值。Run Container是Bitmap的退化处理操作,压缩存储有一定连续值的数据存储。
以下是十进制 821697800 和 191037 的存储方式
由此可以知道,如果计算两个bitmap的交集: {191037,821697800} & {191037}是需要遍历多个Chunk,其中191037所在的Chunk还是一个数组。
如果是计算{0,1}&{1}呢?可以想象到简单很多吧!
Roaring64Map
对于64位整型,Roaring64Map使用map存储:std::map<uint32_t, Roaring> 。key是64位整型的高32位。低32位还是一个RoaringBitmap。所以64位整型的稀疏度更高了!
以上是两个比较稀疏的用户 id 群互相计算,计算方式是遍历左右 Roaring64Map 的每一个 RoaringBitmap,然后依次互相计算,性能比较差。
综上,
字典 编码 就是想把用户真实的bitmap数据进行 压缩 存储,减少使用的 std ::map桶,每个RoaringBitmap中减少Chunk,每个Chunk中尽可能出现Bitmap Container。这样计算时可以用更少的桶 遍历 ,和更快的位运算。
设计疑问
单个普通bitmap最多存储多少个数?
普通bitmap是由数组实现,数据使用bit来占位「二进制0.1」,如果是int数组,使用N/32方式定位数组位置即下标,使用N%32 取余方式定位在int的32bit中的位置。
bitmap 数组 能存储多少数,取决于数组的类型是int/long 和数组的长度。
bitmap为int数组类型,可以存储 32bit × 2^31(数组长度)个数,大约是 10^10 次方,60亿个数
bitmap为long数组类型,可以存储 64bit × 2^31个数, 是上边的2倍。
如下 来计算数组需要多少空间「N代表要存储最大的数值」
int temp[]=new int[1+N/32]
long temp[]=new long[1+N/64]
int/long的最大值?
Java中,int的最大值是 2^31 次方,覆盖负数、正数:-2147483648、2147483647。内存占用4字节; long的最大值是 2^63 次方,覆盖负数、正数;内存占用8字节。
数组 的最大容量?
数组的length的类型是int,所以数组最大容量是 2^31 次方(int的最大值)。
例如int类型的 数组 ,4字节 * 2^31 /1024(2^10)/1024/1024 = 8G,也就是最大8G内存(不用long作为length的类型,是因为没有8字节 * 2^63/1024(2^10)/1024/1024= G 这么大内存的机器)。
延伸:String的最大长度,是char[],最大长度也是 2^31 次方,内存 2字节 * 2^31 = 4G
bitmap存储的数据过于稀疏会怎么样?
普通bitmap 数据稀疏。 又比如要存入(10,8887983,93452134)这三个数据,我们需要建立一个 99999999 长度的 BitMap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入 Roaring BitMap 来解决。
RoaringBitmap数据稀疏
RoaringBitmap中,Array Container存储的是这个数据,需要遍历array,BitMap Container存储的是数据根据取模的占位符,是位计算。当数据过于稀疏时,RoaringBitmap使用Array Container,遍历array,以至于效率不如BitMap Container的位计算。
如何解决数据稀疏问题?字典 编码
数据过于稀疏时,RoaringBitmap 底层存储结构用的是 Array Container 而不是 BitMap Container,Array Container 性能远远差于 BitMap Container。因此我们可以使用全局字典将用户 ID 映射成连续递增的 ID,这样的话数据稠密,就会使用bitmap container,效率会高很多。
字典 编码 /全局字典是为了提升bitmap的计算性能。
原理上讲:字典 编码 就是想把用户真实的bitmap数据进行 压缩 存储,减少使用的 std ::map桶,每个RoaringBitmap中减少Chunk,每个Chunk中尽可能出现Bitmap Container。这样计算时可以用更少的桶 遍历 ,和更快的位运算。
总体原则:分片正交原则
BitEngine中使用了bitmap64类型,它存储id类型,
1 建表的时候如果设置了BitEngineEncode属性,则导入的前置条件是:
保证相同的id出现在相同的物理机上。
2 建表的时候如果没有设置BitEngineEncode属性,则没有任何限制
为什么有这个原则?
BitEngine字典编码是在每台物理机上存储独立的字典,因此如果相同的id导入到不同的物理机上,编码就会不同,也就是同一个id在不同机器上被编码了多次,导致查询结果出现偏差,设计见BitEngine 字典编码
为什么要字典编码?
BitEngine在多个业务的使用中发现,使用字典编码比不使用字典编码性能有50-100倍的提升,跟BitMap64存储原理相关,详见字典编码性能比较
去重工具对比
- HLL
- bloomfilter