Bitmap、RoaringBitmap原理分析
作者:京东科技 曹留界
在人群本地化实践中我们介绍了人群ID中所有的pin的偏移量可以通过Bitmap存储,而Bitmap所占用的空间大小只与偏移量的最大值有关系。假如现在要向Bitmap内存入两个pin对应的偏移量,一个偏移量为1,另一个偏移量为100w,那么Bitmap存储直接需要100w bit的空间吗?数据部将偏移量存入Bitmap时,又如何解决数据稀疏问题呢?本文将为大家解答这个问题。
一、BitMap
Bitmap的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此可以大大节省存储空间。
如果想将数字2存入位图中,则只需要将位图数组中下标为2的数组值置为1。
但是,如果现在要存储两个人群ID对应的偏移量,一个偏移量为1,另一个偏移量为100w,如果将这两个值直接放到位图数组中,那么位图数组所需要的空间就是100wbit,会产生大量的空间浪费。那么有什么方法可以避免空间浪费吗?答案就是RoaringBitMap!
二、RoaringBitMap
RoaringBitMap是一种高效压缩位图,简称RBM。RBM的概念于2016年由S. Chambi、D. Lemire、O. Kaser等人在论文《Better bitmap performance with Roaring bitmaps》 《Consistently faster and smaller compressed bitmaps with Roaring》中提出。下面我们结合java中的实现对其进行介绍。
2.1 实现思路
RBM主要将32位的整型(int)分为高16位和低16位(两个short),其中高16位对应的数字使用16位整型有序数组存储,低16位根据不同的情况选择三种不同的container来存储,这三种container分别为:
•Array Container
底层数据结构为short类型的数组,直接将数字低16位的值存储到该数组中。short类型的数组始终保持有序,方便使用二分查找,且不会存储重复数值。因为这种Container存储数据没有任何压缩,因此只适合存储少量数据。其内部数组容量是动态变化的,当容量不够时会进行扩容,最大容量为4096。由于数组是有序的,存储和查询时都可以通过二分查找快速定位其在数组中的位置。
ArrayContainer占用的空间大小与存储的数据量为线性关系,每个short为2字节,因此存储了N个数据的ArrayContainer占用空间大致为2N字节。存储一个数据占用2字节,存储4096个数据占用8kb。
•Bitmap Container
底层实现为位图。这种Container使用long[]存储位图数据。我们知道,每个Container处理16位整形的数据,也就是0~65535,因此根据位图的原理,需要65536个比特来存储数据,每个比特位用1来表示有,0来表示无。每个long有64位,因此需要1024个long来提供65536个比特。
因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kb。
•Run Container
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
;
源码中的short[] valueslength中存储的就是压缩后的数据。
这种压缩算法的性能和数据的连续性(紧凑性)关系极为密切,对于连续的100个short,它能从200字节压缩为4字节,但对于完全不连续的100个short,编码完之后反而会从200字节变为400字节。
如果要分析RunContainer的容量,我们可以做下面两种极端的假设:
最好情况,即只存在一个数据或只存在一串连续数字,那么只会存储2个short,占用4字节
最坏情况,0~65535的范围内填充所有的奇数位(或所有偶数位),需要存储65536个short,128kb
也就RBM在存入一个32位的整形数字时,会先按照该数字的高16位进行分桶,以确定该数字要存入到哪个桶中。确定好分桶位置后,再将该数字对应的低16位放入到当前桶所对应的container中。
举个栗子
以十进制数字131122为例,现在我们要将该数字放入到RBM中。第一步,先将该数字转换为16进制,131122对应的十六进制为0x00020032;其中,高十六位对应0x0002,首先我们找到0x0002所在的桶,再将131122的低16位存入到对应的container中,131122的低16位转换为10进制就是50,没有超过ArrayContainer的容量4096,所以将低16位直接放入到对应的ArrayContainer中。
如果要插入的数字低16位超过了4096,RBM会将ArrayContainer转换为BitMapContainer。反之,如果数据在删除之后,数组中的最大数据小于4096,RBM会将BitMapContainer转换回ArrayContainer。
RBM处理的是32位的数字,如果我们想处理Long类型的数字怎么办呢?这个时候可以使用Roaring64NavigableMap。Roaring64NavigableMap也是使用拆分模式,将一个long类型数据,拆分为高32位与低32位,高32位代表索引,低32位存储到对应RoaringBitmap中,其内部是一个TreeMap类型的结构,会按照signed或者unsigned进行排序,key代表高32位,value代表对应的RoaringBitmap。
三、空间占用对比
1、连续数据
分别向位图中插入1w、10w、100w、1000w条连续数据,并且对比BitMap和RoaringBitMap占用空间的大小。比较结果如下表所示:
10w数据占用空间 | 100w数据占用空间 | 1000w数据占用空间 | |
---|---|---|---|
BitMap | 97.7KB | 976.6KB | 9.5MB |
RoaringBitMap | 16KB | 128KB | 1.2MB |
@Test public void testSizeOfBitMap() { //对比占用空间大小 - 10w元素 RoaringBitmap roaringBitmap3 = new RoaringBitmap(); byte[] bits2 = new byte[100000]; for (int i = 0; i < 100000; i++) { roaringBitmap3.add(i); bits2[i] = (byte) i; } System.out.println("10w数据 roaringbitmap byte size:"+ roaringBitmap3.getSizeInBytes()); System.out.println("10w数据 位图数组 byte size:"+bits2.length); RoaringBitmap roaringBitmap4 = new RoaringBitmap(); byte[] bits3 = new byte[1000000]; for (int i = 0; i < 1000000; i++) { roaringBitmap4.add(i); bits3[i] = (byte) i; } System.out.println("100w数据 roaringbitmap byte size:"+ roaringBitmap4.getSizeInBytes()); System.out.println("100w数据 位图数组 byte size:"+bits3.length); RoaringBitmap roaringBitmap5 = new RoaringBitmap(); byte[] bits4 = new byte[10000000]; for (int i = 0; i < 10000000; i++) { roaringBitmap5.add(i); bits4[i] = (byte) i; } System.out.println("1000w数据 roaringbitmap byte size:"+ roaringBitmap5.getSizeInBytes()); System.out.println("1000w数据 位图数组 byte size:"+bits4.length); }
运行截图:
2、稀疏数据
我们知道,位图所占用空间大小只和位图中索引的最大值有关系,现在我们向位图中插入1和999w两个偏移量位的元素,再次对比BitMap和RoaringBitMap所占用空间大小。
占用空间 | |
---|---|
BitMap | 9.5MB |
RoaringBitMap | 24Byte |
@Test public void testSize() { RoaringBitmap roaringBitmap5 = new RoaringBitmap(); byte[] bits4 = new byte[10000000]; for (int i = 0; i < 10000000; i++) { if (i == 1 || i == 9999999) { roaringBitmap5.add(i); bits4[i] = (byte) i; } } System.out.println("两个稀疏数据 roaringbitmap byte size:"+ roaringBitmap5.getSizeInBytes()); System.out.println("两个稀疏数据 位图数组 byte size:"+bits4.length); }
运行截图:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【官宣】RuoYi-Vue-Plus 和 RuoYi-Cloud-Plus 加入 Dromara 开源社区
平台简介 RuoYi-Vue-Plus 多租户权限管理系统 使用世面先进技术栈 针对企业痛点 全方位重写 RuoYi-Vue RuoYi-Cloud-Plus 微服务权限管理系统 使用世面先进技术栈 针对企业痛点 全方位重写 RuoYi-Cloud 作者介绍 dromara/RuoYi-Vue-Plus 与 dromara/RuoYi-Cloud-Plus 作者 目前就职某风险测控评估公司 负责公司平台核心架构设计维护工作 参考文档 使用框架前请仔细阅读文档重点注意事项https://javalionli.gitee.io/plus-doc 项目地址 Vue版本 https://gitee.com/dromara/RuoYi-Vue-Plus Cloud版本 https://gitee.com/dromara/RuoYi-Cloud-Plus Vue版本技术栈 前端开发框架 Vue3、Element Plus 后端开发框架 SpringBoot、Undertow 权限认证框架 Sa-Token、Jwt 数据库支持 MySQL、Oracle、PostgreSQL、SQLServer 缓存...
- 下一篇
深入浅出RPC服务 | 不同层的网络协议
导读:✍️ 本系列文章从RPC产生的历史背景开始讲解,涉及RPC核心原理、RPC实现、JSF的实现等,通过图文类比的方式剖析它的内部世界,让大家对RPC的设计思想有一个宏观的认识。 作者:王禹展 京东健康 网络协议 为什么需要网络协议? 网络协议是为计算机网络中进行数据交换而建立的规则、标准或约定的集合。 网络中一个微机用户和一个大型主机的操作员进行通信,由于这两个数据终端所用字符集不同,因此操作员所输入的命令彼此不认识。为了能进行通信,规定每个终端都要将各自字符集中的字符先变换为标准字符集的字符后,才进入网络传送,到达目的终端之后,再变换为该终端字符集的字符。就像我们说话用某种语言一样,在网络上的各台计算机之间也有一种语言,这就是网络协议,不同的计算机之间必须使用相同的网络协议才能进行通信。 一次请求都需要用到那些协议? 1.要传输数据,首先如何知道对应的机器的地址?通过IP可以确认具体的机器(网络层的IP层协议)。 2.找到目标机器后,需要知道该机器上那个程序接受本次请求?通过端口就能确定具体的程序(传输层的TCP层协议)。 3.确定完程序后,怎么区分不同的请求,每一个请求如何关联...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启