作者:京东科技 曹留界

在人群本地化实践中我们介绍了人群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);
}

运行截图:

Bitmap、RoaringBitmap原理分析的更多相关文章

  1. 使用AsyncTask异步更新UI界面及原理分析

    概述: AsyncTask是在Android SDK 1.5之后推出的一个方便编写后台线程与UI线程交互的辅助类.AsyncTask的内部实现是一个线程池,所有提交的异步任务都会在这个线程池中的工作线 ...

  2. Android 4.4 KitKat NotificationManagerService使用具体解释与原理分析(一)__使用具体解释

    概况 Android在4.3的版本号中(即API 18)增加了NotificationListenerService,依据SDK的描写叙述(AndroidDeveloper)能够知道,当系统收到新的通 ...

  3. 【构建Android缓存模块】(一)吐槽与原理分析

    http://my.oschina.net/ryanhoo/blog/93285 摘要:在我翻译的Google官方系列教程中,Bitmap系列由浅入深地介绍了如何正确的解码Bitmap,异步线程操作以 ...

  4. BitMap的原理和实现

    相关概念 基础类型 在java中: byte -> 8 bits -->1字节 char -> 16 bit -->2字节 short -> 16 bits --> ...

  5. Handler系列之原理分析

    上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...

  6. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

  7. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  8. Android中Input型输入设备驱动原理分析(一)

    转自:http://blog.csdn.net/eilianlau/article/details/6969361 话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反 ...

  9. 转载:AbstractQueuedSynchronizer的介绍和原理分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  10. Camel运行原理分析

    Camel运行原理分析 以一个简单的例子说明一下camel的运行原理,例子本身很简单,目的就是将一个目录下的文件搬运到另一个文件夹,处理器只是将文件(限于文本文件)的内容打印到控制台,首先代码如下: ...

随机推荐

  1. 云图说|云数据库RDS跨区域备份

    摘要:云数据库RDS支持将备份文件存放到另一个区域存储,某一区域的实例故障后,可以在异地区域使用备份文件在异地恢复到新的RDS实例,用来恢复业务. 本文分享自华为云社区<云图说_云数据库RDS- ...

  2. 华为云媒体査勇:华为云在视频AI转码领域的技术实践

    摘要:为大家介绍华为云媒体处理服务在视频AI转码领域的一些技术实践. 随着5G的落地和消费终端的不断升级,消费环节对视频画质的要求也越来越高,为了给消费者带来更清晰.更逼真和更具沉浸感的观感体验,对云 ...

  3. 用火山引擎DataTester,这家企业开始了“数据驱动增长”

    年末购物季已至,近些年来,预售抵扣.平台满减.品类专享券.大额补贴--动辄四五种计算方法叠加的大促活动,让不少消费者"懵"感十足.同一样商品,到底谁家卖的最便宜?比价平台应声发展而 ...

  4. AI 0基础学习,数学名词解析

    AI学习过程中,常见的名词解析 中位数 将数据从小到大排序,奇数列,取中间值,偶数列,中间两个值的平均,可做为销售指标 众数 一组数据中,数值出现最多的那个.反映哪款产品,销量最好 平均数 比赛中,去 ...

  5. Intellij IDEA 关闭阿里编码规约“请不要使用行尾注释”提醒

    Settings -> Inspections -> 注释 取消 "方法内部单行注释 xxxx " 里面的勾,[设完后重启]如下图

  6. 【计算机网络】soap和rest简单比较整理

    https://www.bilibili.com/video/BV1ht411U7fC/?spm_id_from=333.337.search-card.all.click&vd_source ...

  7. 路径规划之 A* 算法

    算法介绍 A*(念做:A Star)算法是一种很常用的路径查找和图形遍历算法.它有较好的性能和准确度.本文在讲解算法的同时也会提供Python语言的代码实现,并会借助matplotlib库动态的展示算 ...

  8. U64949 棋盘覆盖(二分图)| 二分图匹配总结

    https://ac.nowcoder.com/acm/contest/1062/B [题目] 给出一张n×n(n≤100)的国际象棋棋盘,其中被删除了一些点,问可以使用多少1*2的多米诺骨牌进行掩盖 ...

  9. 反应式编程 RxJava 设计原理解析

    本文首发于 vivo互联网技术 微信公众号 链接: https://mp.weixin.qq.com/s/duO1pAfaKUI2_x_GVvZHMg作者:Yunjie Ma 一.ReactiveX ...

  10. 智慧地产-售楼中心 3D 沙盘可视化

    前言 随着"互联网+房地产"走入全国各大地产项目,房企依托互联网将房地产从传统地产转向智慧地产已然是眼下用户最欢迎的转型模式.智慧地产是由智慧社区.智慧园区.智慧公寓及智能家居等组 ...