RoaringBitmap数据结构及原理

首先

RoaringBitmap数据结构

请注意,图中的short[] keys为早期版本,2019年底之后改为char[] keys,下面有介绍。

每个RoaringBitmap(GitHub链接)中都包含一个RoaringArray,名字叫highLowContainer
highLowContainer存储了RoaringBitmap中的全部数据。

RoaringArray highLowContainer;

这个名字意味着,会将32位的整形(int)拆分成高16位和低16位两部分(两个short)来存储。

RoaringArray的数据结构很简单,核心为以下三个成员:

char[] keys;
Container[] values;
int size;

每个32位的整形,高16位会被作为key存储到char[] keys中(可以按short[] keys来理解),低16位则被看做value,存储到Container[] values中的对应Container中。keysvalues通过下标一一对应。size则标示了当前包含的key-value pair的数量,即keysvalues中有效数据的数量。

keys数组永远保持有序,方便二分查找。

关于short与char

在2019年底之前的版本,keys为short[]类型。但Java的short为有符号数,边界为[-32768, 32767],作为下标使用时,负值部分空间浪费,而正值部分在靠近32767时需要非常谨慎,稍不注意就会溢出,出现BUG。因此,改为了天生无符号的char来代替short,同样是2字节,边界提升为[0, 65535],空间就豁然开朗了,减少了大量的边界处理代码,是一种非常聪明的做法。

在下文介绍Container时,也可以看到同样的技巧。

相关pull request链接: https://github.com/RoaringBitmap/RoaringBitmap/pull/364

三种Container

下面介绍到的是RoaringBitmap的核心,三种Container。

通过上面的介绍我们知道,每个32位整形的高16位已经作为key存储在RoaringArray中了,那么Container只需要处理低16位的数据。

ArrayContainer

private static final int DEFAULT_INIT_SIZE = 4;
static final int DEFAULT_MAX_SIZE = 4096;

char[] content;

结构很简单,只有一个char[] content(同样可以理解为short[]),将value直接存储。

char[] content始终保持有序,方便使用二分查找,且不会存储重复数值。

因为这种Container存储数据没有任何压缩,因此只适合存储少量数据。

ArrayContainer占用的空间大小与存储的数据量为线性关系,每个char(short)为2字节,因此存储了N个数据的ArrayContainer占用空间大致为2N字节。存储4096个数据占用8KB。

根据源码可以看出,常量DEFAULT_MAX_SIZE值为4096,当容量超过这个值的时候会将当前Container替换为BitmapContainer。

BitmapContainer

public static final int MAX_CAPACITY = 1 << 16;

final long[] bitmap;
int cardinality;

public BitmapContainer() {
  this.cardinality = 0;
  this.bitmap = new long[MAX_CAPACITY / 64];
}

这种Container使用long[]存储位图数据。我们知道,每个Container处理16位整形的数据,也就是0~65535,因此根据位图的原理,需要65536个比特来存储数据,每个比特位用1来表示有,0来表示无。每个long有64位,因此需要1024个long来提供65536个比特。

因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kB。

RunContainer

private char[] valueslength;

int nbrruns = 0;

RunContainer中的Run指的是行程长度压缩算法(Run Length Encoding),对连续数据有比较好的压缩效果。

它的原理是,对于连续出现的数字,只记录初始数字和后续数量。即:

  • 对于数列11,它会压缩为11,0
  • 对于数列11,12,13,14,15,它会压缩为11,4
  • 对于数列11,12,13,14,15,21,22,它会压缩为11,4,21,1

源码中的char[] valueslength中存储的就是压缩后的数据。

这种压缩算法的性能和数据的连续性(紧凑性)关系极为密切,对于连续的100个char(short),它能从200字节压缩为4字节,但对于完全不连续的100个char,编码完之后反而会从200字节变为400字节。

如果要分析RunContainer的容量,我们可以做下面两种极端的假设:

  • 最好情况,即只存在一个数据或只存在一串连续数字,那么只会存储2个char,占用4字节
  • 最坏情况,0~65535的范围内填充所有的奇数位(或所有偶数位),需要存储65536个char,128KB

Container性能总结

读取时间

只有BitmapContainer可根据下标直接寻址,复杂度为O(1),ArrayContainer和RunContainer都需要二分查找,复杂度O(log n)

内存占用

Container内存比较

这是我画的一张图,大致描绘了各Container占用空间随数据量的趋势(图中k指的是KB,请谅解)。

其中,

  • ArrayContainer一直线性增长,在达到4096后就完全比不上BitmapContainer了
  • BitmapContainer是一条横线,始终占用8KB
  • RunContainer比较奇葩,因为和数据的连续性关系太大,因此只能画出一个上下限范围。不管数据量多少,下限始终是4字节;上限在最极端的情况下可以达到128KB。

RoaringBitmap针对Container的优化策略

创建时:

  • 创建包含单个值的Container时,选用ArrayContainer
  • 创建包含一串连续值的Container时,比较ArrayContainer和RunContainer,选取空间占用较少的

转换:

针对ArrayContainer:

  • 如果插入值后容量超过4096,则自动转换为BitmapContainer。因此正常使用的情况下不会出现容量超过4096的ArrayContainer
  • 调用runOptimize()方法时,会比较和RunContainer的空间占用大小,选择是否转换为RunContainer。

针对BitmapContainer:

  • 如果删除某值后容量低至4096,则会自动转换为ArrayContainer。因此正常使用的情况下不会出现容量小于4096的BitmapContainer
  • 调用runOptimize()方法时,会比较和RunContainer的空间占用大小,选择是否转换为RunContainer。

针对RunContainer:

  • 只有在调用runOptimize()方法才会发生转换,会分别和ArrayContainer、BitmapContainer比较空间占用大小,然后选择是否转换。

以上

  • 26
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值