关于bitmap:BitmapRoaringBitmap原理分析
作者:京东科技 曹留界 在人群本地化实际中咱们介绍了人群ID中所有的pin的偏移量能够通过Bitmap存储,而Bitmap所占用的空间大小只与偏移量的最大值有关系。如果当初要向Bitmap内存入两个pin对应的偏移量,一个偏移量为1,另一个偏移量为100w,那么Bitmap存储间接须要100w bit的空间吗?数据部将偏移量存入Bitmap时,又如何解决数据稠密问题呢?本文将为大家解答这个问题。 一、BitMapBitmap的根本思维就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。因为采纳了Bit为单位来存储数据,因而能够大大节俭存储空间。 如果想将数字2存入位图中,则只须要将位图数组中下标为2的数组值置为1。 然而,如果当初要存储两个人群ID对应的偏移量,一个偏移量为1,另一个偏移量为100w,如果将这两个值间接放到位图数组中,那么位图数组所须要的空间就是100wbit,会产生大量的空间节约。那么有什么办法能够防止空间节约吗?答案就是RoaringBitMap! 二、RoaringBitMapRoaringBitMap是一种高效压缩位图,简称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数据占用空间BitMap97.7KB976.6KB9.5MBRoaringBitMap16KB128KB1.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); }运行截图: ...