Roaring bitmaps

最近看一篇文章,里面涉及到使用roaring bitmaps来推送用户广告并通过计算交集来降低用户广告推送次数。本文给出roaring bitmaps的原理和基本用法,后续给出原文的内容。

本文来自:A primer on Roaring bitmaps: what they are and how they work

我从这篇解决大规模留存分析的文章中了解到了Roaring bitmaps,使用Roaring bitmaps而非传统的bitmaps可以将应用使用的内存从~125G下降到300M,节省了99.8%的内存资源。

但这是如何做到的?

下面是两篇与Roaring bitmaps相关的论文:

  1. This one proposes the data structure.
  2. This one introduces a critical optimization.

本文介绍了什么是bitmaps及其用途,什么是Roaring bitmaps以及它是如何解决传统bitmaps中存在的问题的,并一步步揭示Roaring bitmaps的顶层机构及其工作方式。

bitmaps采用了许多算法、技术和启发式方法,这里不作详细介绍,这些细节对理解Roaring bitmaps的基本内部结构和操作并不重要。

什么是bitmaps,bitmaps解决什么问题

Bitmaps 是一个bits位数组,用于存储整数集。

当集合中添加了一个整数N之后,会将第N个bit位设置为1,如下图所示:

图1:bitmaps的运作展示

通过这种存储整数的方式,可以非常快速地使用CPU的位与和位或命令分别计算集合的交集和并集。

事实证明,对于很多查询和数据库应用来说,快速计算集合的交集和并集至关重要。查询和数据库索引中存在各种操作,这些操作可以归结为需要快速计算出交集或并集的两组整数集。

以反向查询索引为例:

  • 假设你已经为数十亿个文档设置了索引,且每个文档都有一个整数id
  • index maps terms表示包含特定词语的一组文档。如pigeon存在于id为{2, 345, 2034, ...}的一组文档中。
  • 使用集合操作来查询多个terms。如为了计算出 carrier AND pigeon,你需要找出包含carrier的文档集合和包含pigeon的文档集合的交集。
  • 使用位操作可以很快地进行集合操作。对于上述例子,只需要执行位与操作就可以找出表示文档id的bit位。

但bitmaps在大规模整数集合场景下的压缩效果不佳。

什么是Roaring bitmaps

roaringbitmap.org中有如下介绍:

Roaring bitmaps是一种压缩的bitmaps,它比bitmaps快百倍。

Roaring bitmaps是一种优化的bitmaps,它和传统的bitmaps一样,都为整数提供了一种集合数据结构。可以插入整数,校验整数的存在性,以及获取两个整数集合的交集和并集等。

相比传统的bitmaps,Roaring bitmaps提供了更好的压缩效果。更重要的是,采用这种方式并不会对性能造成显著的影响。

roaringbitmap.org 中列举了使用Roaring bitmaps 的OLAP数据库和查询系统。

Roaring bitmaps解决了哪些传统bitmaps无法解决的问题?

对于一个稀疏集合,传统的bitmaps的压缩效果较差。

假设一个传统bitmaps为空,添加一个整数8,000,000,此时:

  • 首先分配1,000,000 字节的空间
  • 然后将第8,000,000个bit位设置为1,如下图所示:

图2:如果在一个空的bitmaps中直接分配第800万个bit位,会发生什么。

这种方式会出现如下问题:

  • bitmaps中只设置了一个整数
  • 而一个整数最多需要4个字节
  • 但传统的bitmaps却使用了1M字节的内存,比所需的内存多了6个数量级。

Roaring bitmaps可以在解决该问题的同时保证集合操作的快速性。

先前的很多研究也试图解决bitmaps压缩性较差的问题,并取得了令人印象深刻的结果,但代价是集合操作的性能。

Roaring bitmap是如何工作的

Roaring bitmap使用了多种方式来改善传统bitmaps的性能。

Part 1: Roaring bitmaps 的内存布局

所有32位整数都被划分为连续的块(chunk)

图3:如何在Roaring bitmap中将32位的整数空间划分为chunk

Roaring bitmaps最多可以支持2^16个chunks,每个chunk共享相同的16个最高有效位(Msb),

如上图所示,Roaring bitmaps使用的分区方案可以确保一个整数始终属于2^16(或65536)个连续整数所在的某个chunk。

注意:此外还有64位的Roaring bitmaps实现,本文不对此做深入讨论。

chunks是Roaring bitmaps中对整数的逻辑划分。属于一个chunk的所有整数在物理上都保存在相同的container中。

图4:来自第一篇Roaring bitmap 论文中的3个containers的例子。

cardinality表示元素个数。

上图展示了3个不同的chunks,对应的3个不同的containers。一个chunk能且只能对应Roaring bitmap中的一个container。

如果将62的倍数的前1000个元素插入到Roaring位图中,那么它们将最终位于图4最左边的容器中。这个容器的cardinality为1000。如果后续插入了整数63,则会落入相同的container中,容器的cardinality将是1001。

后续可以看到,container的cardinality决定了它在内存中的表达方式。

稀疏containers:包含<=4096个整数,它们存储为有序的压缩数组。

图4中最左和中间的两个containers(cardinalities为1000和100)是稀疏的,因此它们将被存储为16位整数的有序压缩数组

通过压缩,可以将32位稀疏压缩为16位整数,见下图:

图5:图2中的两个稀疏Roaring bitmap container,以及它们如何在内存中存储的示例。

每个container最多可以保存2^16个不同的整数。为了从稀疏container中获取原始的32位整数,可以将16位整数和container的高16位组合起来获取原始整数。

这些数组是动态分配的,因此一个稀疏container中的内存会随着整数的累计而增加。

密集容器:包含>4096个整数,它们被存储为bitmaps。

图4中最右边的container为密集型container(cardinality 为2^15),因此它会被存储为传统的bitmaps。

密集containers为bitmaps,包含2^16位(8KB)的bitmaps,直接分配存储。bitmaps中的第N个bit位对应chunk中的第N个整数。

一级索引指向所有容器,索引存储为有序数组。

一级索引中存储了Roaring bitmap中每个container的高16位,以及指向对应container的指针。

图7:一级索引中指向图2、3和4中描述的containern的指针

索引存储为有序数组,并随着Roaring bitmap中containers的增加而动态增长。

Part 2: Roaring bitmaps中的集合操作

整数的插入会因container类型而异,可能会导致container的类型发生变化。

为了插入整数N,首先获取N的高16位(N/2^16),并在Roaring bitmap中找到N对应的container。

Array container和bitmap container的插入操作不同:

  • Bitmap container:将第N % 2^16个bit位设置为1。注意bitmap是直接分配的。
  • Array container:在有序数组的第N % 2^16个位置插入N。注意数组是动态分配的,随数据的增加而增加。

插入操作可能会改变container的类型,例如一个Array container中有4096个整数,则插入操作会将其转换为一个bitmap container,然后将第N % 2^16个bit位设置为1。

如果一个container不存在,则会首先创建一个新的Array container,然后将其加入Roaring bitmap的一级索引中,最后将N添加到Array container中。

校验数值的存在性会随container类型而异

为了校验是否存在整数N,首先获取N的高16位(N % 2^16),然后用它在Roaring bitmap中找到对应的container。

如果container不存在,则N也不存在。

Array container和bitmap container的存在性校验方式不同:

  • Bitmap container:校验第N % 2^16个bit位是否为1
  • Array container:使用二分法在有序数组中找到第N % 2^16个位置的值

计算两个Roaring bitmaps的交集。算法会因container类型而异,且container类型也可能发生变化。

为了计算Roaring bitmaps A和B的交集,只需要计算A和B中匹配的containers的交集即可。匹配的container为两个Roaring bitmaps中高16位相同的container,即相同的chunk。

交集运算会随container的类型而异,分为:

  • Bitmap / Bitmap: 计算两个Bitmaps的位与即可。如果cardinality<=4096,则将结果保存在Array container中,否则保存在bitmap container中。
  • Bitmap / Array: 遍历数组,然后在bitmap中校验每个16位整数的存在性。如果整数存在,则将其添加到一个Array container中。注意Bitmap和array container的交集总是会创建出一个array container。
  • Array / Array: 两个array containers的交集总是会生成一个新的array container。交集的运算性能会随着cardinality变化(此篇论文的第5页底部有描述),可以是简单的合并(和merge sort的方式相同)或快速交集(参见该论文)。

如果一个Roaring bitmap中的某个container没有对应的container,则不会出现在结果中,即交集为空。

Roaring bitmap 的并集。算法会随container类型而异,container类型也可能变化

为了计算Roaring bitmaps A和B的并集。需要计算A和B中匹配containers的并集。

并集运算可能会因container类型而异,有如下几种:

  • Bitmap / Bitmap: 计算两个bitmaps的位或。两个bitmap container的并集总是会创建另一个bitmap container。
  • Bitmap / Array: 复制bitmap,并在该bitmap中为array container中的所有整数设置bit位。bitmap和array container的并集总是会创建另一个bitmap container。
  • Array / Array: 如果两个array container的cardinalities总数<=4096,则生成的container会是一个array container。这种情况下,会将两个arrays中的所有整数添加到一个新的array container中。否则会假设生成的container是一个bitmap:创建一个新的bitmap container,然后在该bitmap中为两个array containers中的整数设置bit位。如果生成的container的cardinality<=4096,则将该bitmap container转换为一个array container。

最后,将A和B中没有匹配container的所有containers添加到结果中。

Part 3:第三种也是最后一种container类型——"run" container——如何优化大量连续的整数

part 1和2中涵盖了Roaring bitmaps的大分部内部结构和操作。最后讨论一下Roaring bitmaps的第二篇论文中的一个重要优化。

run container为使用两个16位整数表示的连续整数:run开始和run长度。

第二篇论文的第3页有如下表述:

新容器在概念上很简单:给定一个run(例如[10,1000]),我们存储起点(10)及其长度减1(990)。然后将起点和长度成对打包,开始值和长度值都为16位整数。

这种技术称为run-length编码。Run-length可以有效压缩bitmaps,但在很多场景下,却降低了set操作的性能。

当客户端调用runOptimize函数时,run container是显式形成的,而在某些情况下,当向Roaring bitmap中添加了大范围数值时,则是隐式形成的。

与稀疏和密集container不同,run container通常不会自动形成。

  1. 客户端可以调用runOptimize来优化Roaring bitmap中的大量连续整数,这种情况下,run container可能会替代现有的array 或 bitmap container。
  2. Roaring bitmap提供了一个添加连续数值的操作,这种情况下,可能会形成run container。

该篇论文没有具体规定如何以及合时会发生第二种场景。可能场景是,为一个还没有container的chunk添加了一段连续的值,那么此时创建一个run container(而不是array或bitmap container)可能更有意义。

runOptimize仅在run container小于要替换的container时才会创建该container。

runOptimize首先会计算一个container中的连续值的数量。然后再决定是否需要创建一个run container:run container必须要小于等同的array或bitmap container。

第2篇论文的第6,7页描述了一种用于计算连续值数量的算法:

run container的添加为所有集合操作引入了新的算法。

Roaring bitmaps论文中并没有描述run container的插入和校验整数存在性的算法:这些操作相对简单。

但是,添加run container需要为如下组合实现高性能并集和交集算法:

  • Run / Run
  • Run / Array
  • Run / Bitmap

这里不再作深入讨论,这些算法也不会太复杂(参见该论文的第10页)。

Roaring bitmaps使用了多种算法和技术,与其他bitmaps实现相比,可以实现更好的压缩效果和更快的性能。

Roaring bitmaps的实现很有挑战性,但它的表现却很好,尤其是在OLAP工作负载中使用时。创建者设法根除常见的多种场景中存在的低效率问题——稀疏数据、密集数据、大量连续的数据——并且同时解决了所有这些问题。

第3篇论文描述了创建者使用C语言编写的一个实现,该实现利用了他们使用SIMD(单指令多数据)指令设计的矢量化算法。这里提供了该实现、CRoaring以及其他多种语言的实现。它们被用于主流的柱状数据库和搜索应用程序,并得到了积极的维护、改进和优化。

Golang的roaring bitmaps

Roaring bitmaps可以实现整数集合交集并集运算,并在保证数据压缩效果的同时同时保证了运算的高效性。

这里给出了golang版本的实现。分为32位64位两种。需要注意的是bitmaps并不是goroutines安全的。下面32位的Roaring bitmaps为例看下bitmap container和array container是如何添加数据的。

在上文中有讲,当container为bitmaps类型时,会直接分配存储,从下面bitmap container的初始化中可以看到,其初始化会直接分配65535 bit位的存储空间。当bitmap存储满后,会被压缩为run container。

func newBitmapContainer() *bitmapContainer {
p := new(bitmapContainer)
size := (1 << 16) / 64
p.bitmap = make([]uint64, size, size)
return p
}

而array container中主要用于存储稀疏数值。下面是在array container中添加数值的函数。可以看到array container并不是预先分配的,它随添加的数值的增加而增加。


func (ac *arrayContainer) iaddReturnMinimized(x uint16) container {
// Special case adding to the end of the container.
l := len(ac.content)
// arrayDefaultMaxSize为4096。下面表示如果当前container中的数值总数没有超过最大值,
// 且要添加的值x大于有序数组的最后一个时,只需要将x追加到有序数组的最后一个即可
if l > 0 && l < arrayDefaultMaxSize && ac.content[l-1] < x {
ac.content = append(ac.content, x)
return ac
} // 使用二分法找到x或插入x的位置
loc := binarySearch(ac.content, x) // 如果loc<0表示没有在container中找到x,如果当前container中的数值总数为arrayDefaultMaxSize,
// 则需要转换为bitmap container,然后再添加x。
// 否则根据找到的位置loc,再在array container中插入x
if loc < 0 {
if len(ac.content) >= arrayDefaultMaxSize {
a := ac.toBitmapContainer()
a.iadd(x)
return a
}
s := ac.content
i := -loc - 1
s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = x
ac.content = s
}
return ac
}

Roaring bitmaps的更多相关文章

  1. Frame of Reference and Roaring Bitmaps

    https://www.elastic.co/cn/blog/frame-of-reference-and-roaring-bitmaps http://roaringbitmap.org/ 2015 ...

  2. Elasticsearch 通关教程(七): Elasticsearch 的性能优化

    硬件选择 Elasticsearch(后文简称 ES)的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件../config/elasticsearch. ...

  3. Elasticsearch索引原理

    转载 http://blog.csdn.net/endlu/article/details/51720299 最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计 ...

  4. Elasticsearch-基础介绍及索引原理分析(转载)

    最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计查询的方案设计工作,花了些时间学习Elasticsearch的基础理论知识,整理了一下,希望能对Elastic ...

  5. ElasticSearch原理

    Elasticsearch-基础介绍及索引原理分析 最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计查询的方案设计工作,花了些时间学习Elasticsearc ...

  6. Elasticsearch-基础介绍及索引原理分析

    介绍 Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 L ...

  7. Elasticsearch基本原理分析

    最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计查询的方案设计工作,花了些时间学习Elasticsearch的基础理论知识,整理了一下,希望能对Elastic ...

  8. elasticsearch简介和倒排序索引介绍

    介绍 我们为什么要用搜索引擎?我们的所有数据在数据库里面都有,而且 Oracle.SQL Server 等数据库里也能提供查询检索或者聚类分析功能,直接通过数据库查询不就可以了吗?确实,我们大部分的查 ...

  9. BitMap与RoaringBitmap、JavaEWAH

    本文主要介绍BitMap的算法思想,以及开源工具类JavaEWAH.RoaringBitmap的简单用法. 一.BitMap 介绍 BitMap使用bit位,来标记元素对应的Value.该算法能够节省 ...

  10. Elasticsearch 技术分析(九):Elasticsearch的使用和原理总结

    前言 之前已经分享过Elasticsearch的使用和原理的知识,由于近期在公司内部做了一次内部分享,所以本篇主要是基于之前的博文的一个总结,希望通过这篇文章能让读者大致了解Elasticsearch ...

随机推荐

  1. .NET6.0实现IOC容器

    .NET6.0实现IOC容器 IOC的作用这里省略-只对如何使用进行说明. 1. 创建一个.NET6应用程序 这里使用 .NET6.0 WebAPI 应用 2. 声明接口 public interfa ...

  2. WebKit Inside: CSS 样式表的匹配时机

    WebKit Inside: CSS 的解析 介绍了 CSS 样式表的解析过程,这篇文章继续介绍 CSS 的匹配时机. 无外部样式表 内部样式表和行内样式表本身就在 HTML 里面,解析 HTML 标 ...

  3. Oracle中的substr()函数和INSTR()函数和mysql中substring_index函数字符截取函数用法:计算BOM系数用量拼接字符串*计算值方法

    最近一直在研究计算产品BOM的成本系数,将拼接的元件用量拼接后拆分计算是个问题,后来受到大佬在mysql中截取字符串的启发在oracle中以substr和instr实现了  1.以下是我在mysql中 ...

  4. 中华人民共和国企业所得税月(季)度预缴纳税申报表(A类,2018年版)

    企业按照<中华人民共和国公司法>有关规定整体改制,包括非公司制企业改制为有限责任公司或股份有限公司,有限责任公司变更为股份有限公司,股份有限公司变更为有限责任公司,原企业投资主体存续并在改 ...

  5. 【Cucumber】关于BDD自然语言自动化测试的语法总结

    1.关键字 - Feature 每一个.feature文件必须以关键字Feature开始,Feature关键字之后可以添加该feature的描述,其作用类似于注释,仅仅为了便于理解沟通交流,描述内容中 ...

  6. Opencv系列之一:简介与基本使用

    1 Opencv简介 Opencv是计算机视觉中经典的专用库,其支持多语言,跨平台,功能强大.Opencv-Python为Opencv提供了Python接口,使得使用者在Python中能够调用C/C+ ...

  7. 代码的艺术-Writing Code Like a Pianist

    前言 如何评定一个系统的质量?什么样的系统或者软件可以称之为高质量?可以从三个角度来看,一是架构设计,例如技术选型.分布式系统中的数据一致性考虑等,二是项目管理,无论是敏捷开发还是瀑布式开发,都应当对 ...

  8. 轻巧的批量图片压缩工具imgfast

    现在的手机拍照动辄2M3M,还有7M8m的,如果要把这些文件上传到网上应用,浪费网络,占用资源 所以2022年中秋写了这个小工具,可以批量进行图片文件压缩,支持jpg和png. 文件下载链接https ...

  9. 【matplotlib 实战】--雷达图

    雷达图(Radar Chart),也被称为蛛网图或星型图,是一种用于可视化多个变量之间关系的图表形式.雷达图是一种显示多变量数据的图形方法.通常从同一中心点开始等角度间隔地射出三个以上的轴,每个轴代表 ...

  10. 【Qt6】列表模型——几个便捷的列表类型

    前面一些文章,老周简单介绍了在Qt 中使用列表模型的方法.很明显,使用 Item Model 在许多时候还是挺麻烦的--要先建模型,再放数据,最后才构建视图.为了简化这些骚操作,Qt 提供了几个便捷类 ...