Roaring位图具有更好的位图性能

3 篇文章 0 订阅
1 篇文章 0 订阅

前言

翻译论文,与自己的理解,如有不对的,欢迎评论指出。
Better bitmap performance with Roaring bitmaps
作者:Samy Chambi, Daniel Lemire, Owen Kaser, Robert Godin

摘要

位图索引通常在数据库和搜索引擎中使用。通过利用位级并行性,它们可以明显加速查询。 但是,它们会占用大量内存,因此我们可能更喜欢压缩的位图索引。在Oracle的引领下,位图通常使用游程长度编码(RLE,run-length encoding)进行压缩。在先前工作的基础上,我们介绍了Roaring压缩位图格式:它使用压缩数组而不是RLE进行压缩。我们将其与两种基于RLE的高性能位图编码技术进行了比较:WAH(字对齐混合压缩方案)和Concise(压缩的’n’个可组合整数集)。在合成数据和真实数据上,我们发现咆哮位图(1)的压缩效果通常要好得多(例如2×),而(2)则比压缩的替代品要快(相交的速度最高可达900x)。我们的结果挑战了基于RLE的位图压缩的观点,且它的效果最好。

关键字:性能 测量 索引压缩 位图索引

1. 介绍

位图(bitmp or bitset)是一个二进制数组,我们可以将其视为整数集S的高效紧凑表示形式。给定n位的位图,如果集合中存在[0,n-1]范围内的第i个整数,则将第i位设置为1。例如,集合{3,4,7}和{4,5,7}可能以二进制形式存储为10011000和10110000。我们可以使用位图上的按位运算(AND,OR)来计算两个这样的对应列表之间的并集或交集(例如,在我们的示例中为10111000和10010000)上。位图是Java平台(java.util.BitSet)的一部分。

当S的基数与无穷大n比较大时(例如64位处理器上的| S |> n / 64),位图通常优于其他可比较的数据结构,例如数组,哈希集或树。 但是,在中等低密度位图(n / 10000 <| S | <n / 64)上,压缩的位图(例如Concise)可能更可取[1](这个中等密度是多在,这个是引用另一篇论文的)。

最近提出的大多数压缩位图格式都源自Oracle的BBC[2],并使用行程编码(RLE)进行压缩:WAH [3],Concise [1],EWAH [4],COMPAX [5],VLC [ 6],VAL-WAH [7]等。Wu et al的WAH可能是最著名的。 WAH将n位的位图划分为

⌈ n w − 1 ⌉ 其 中 w 是 字 长 , 例 w = 32 ; w − 1 是 原 因 有 一 位 是 符 号 位 , 只 用 到 正 数 \left \lceil \frac{n}{w-1} \right \rceil \quad 其中w是字长,例w=32;w-1是原因有一位是符号位,只用到正数 w1nww=32w1
WAH区分两种类型的words:仅由w − 1个(11···1)或仅由w−1个零(00···0)组成的单词是fill words,而包含0和1的混合word (例如101110…1)是 literal wordsLiteral words使用w位存储:最高有效位设置为零,其余位存储异构w − 1位。
fill words的序列(全1或全零)也使用w位存储:最高有效位设置为1,第二高有效位指示同质填充字序列的位值,而其余w-2位存储游程长度。

当压缩稀疏位图时,如集合{0,2(w-1), 4(w-1), 6(w-1)…2n(w-1)}; n > 0的整数,w为字。WHA得用2n字。

(n+1) *2w, n>01w, n=0

Concise把内存的使用减少了一半。除了fill words编码外,它使用了类似的编码格式,Concise[2]仅仅使用w-2-[log2(w)]位存储序列运行时长度替代了WAH中存储运行长度为r时使用的w-2位,同时它预留了[log2(w)]位作为位置位。这些[log2(w)]位编码了数字p,其中p是[0,w)集合的一个元素。当p=0时,我们解码为r+1个fill words。当它非零时,我们解码为r个fill words并将它的第(p − 1)位与其他位相比进行0、1反转。在Concise算法中,我们把bitmap看作是一个整数集合,也就是说如果此bitmap是一个literal word,那么位数则代表整数的个数,固定为31个;如果此bitmap是一个fill word,那么整数的个数为Count(31-bits group) * 31。考虑w = 32的情况。Concise可以对集合{0,62,124,。。。。}仅使用32位/整数,而WAH则需要64位/整数。(concise的部分得另外再看)

尽管它们减少了内存使用量,但与未压缩的位图相比,这些源自BBC的格式的随机访问速度较慢。 也就是说,检查或更改第i位值的时间复杂度是O(n)。 因此,尽管它们代表整数集,但我们无法快速检查整数中是否包含整数。 这使得它们不适用于某些应用[8]。此外,RLE格式快速跳过数据的能力有限。例如,假设我们正在计算两个压缩位图之间的按位与。 如果一个位图具有很长的0,我们可能希望跳过其他位图中的相应单词。 没有辅助索引,使用WAH和Concise之类的格式可能无法实现。

代替使用RLE和牺牲随机访问,我们建议将空间[0,n)划分为块,并以不同的方式存储密集和稀疏的块[9]。 在此基础上,我们引入了一种称为Roaring的新位图压缩方案。 咆哮的位图在紧凑高效的两级索引数据结构中存储32位整数。 密集块使用位图进行存储; 稀疏块使用16位整数的压缩数组。 在我们的示例({0,62,124,…})中,它将仅使用≈16位/整数,是Concise内存使用量的一半。 此外,在Colantonio和Di Pietro [1]提出的综合数据测试中,它至少比WAH和Concise快四倍。 在某些情况下,速度可能会快数百倍。

我们的做法让人想起O’Neil和O’Neil的RIDBit外部内存系统。 RIDBit是位图的B-树,当块的密度太小时,将使用列表代替。 但是,与基于WAH的系统FastBit相比,RIDBit的表现不佳[10]:FastBit的速度提高了10倍。 与O’Neil等人的负面结果相反,我们发现在内存中处理时,Roaring位图可能比WAH位图快几倍。 因此,我们的主要贡献之一就是挑战这种信念(例如Colantonio和Di Pietro [1]等作者所表示的),即WAH位图压缩是最有效的选择。

Roaring位图性能的关键因素是新的位计数处理器指令(例如popcnt),该指令最近在台式机处理器上可用(2008年)。 以前,在诸如RIDBit [11]之类的系统中经常使用表查找,但它们的速度可能慢几倍。 这些新指令使我们能够快速计算新块的密度,并有效地从位图中提取set位的位置。

为了超越基于RLE的格式(例如WAH和Concise),我们还依赖于几种算法策略(请参见第4部分)。 例如,当相交两个稀疏块时,我们可以使用基于二分查找的方法,而不是像RIDBit这样的线性时间合并。 同样,当合并两个块时,我们预测结果是密集的还是稀疏的,以最大程度地减少浪费的转换。 相反,O’Neil等。 报告说,RIDBit在计算了块之后将其转换[11]。

2 Roaring 位图

我们将32位索引([0,n))的范围划分为2个16个整数的块,它们共享相同的16个最高有效数字。 我们使用专门的容器来存储它们的16个最低有效位。

当一个块包含不超过4096个整数时,我们使用打包16位整数的排序数组。 当有超过4096个整数时,我们使用2个16位位图。 因此,我们有两种类型的容器:用于稀疏块的数组容器和用于密集块的位图容器。 4096个阈值可确保在容器级别上,每个整数使用的位数不超过16位:对于2个16位以上的4096个整数,我们使用小于16位/整数,否则,我们将精确地使用16位/整数 。

容器以共享的16个最高有效位存储在动态数组中:用作第一级索引。数组将容器按16个最高有效位排序。我们期望此一级索引通常较小:当n = 1000000时,最多包含16个条目。 因此,它通常应保留在CPU缓存中。 容器本身的消耗绝不能超过8kB。

为了说明数据结构,请考虑62的前1000个倍数,所有整数[2 16,2 16 + 100)和所有偶数[2×2 16,3×2 16)的列表。 使用简明格式对该列表进行编码时,我们为1000的62的倍数中的每一个使用一个32位填充词,我们使用两个附加的填充词来包括2 16到2 16 + 100之间的数字列表,以及偶数 [2×2 16,3×2 16)中的数字存储为原义单词。 在Roaring格式中,[2 16,2 16 + 100)中的62的倍数和整数都使用数组容器存储,每个整数使用16位。 [2×2 16,3×2 16)中的偶数存储在2 16位位图容器中。 参见图1。

每个Roaring容器都使用计数器跟踪其基数(整数数)。 因此,可以快速完成Roaring位图的基数计算:最多只求和即可? n / 2 16? 柜台。 与典型的位图相比,它还可以更快地支持等级查询和选择查询:等级查询计算范围为[0,i]的设置位数,而选择查询则查找第i个设置位的位置。

容器和动态数组带来的开销意味着我们的内存使用量可以超过16位/整数。 但是,只要容器的数量比整数的总数量少,我们使用的位就绝不能超过16位/整数。 我们假设容器比整数少得多。 更准确地说,我们假设密度通常超过0.1%或n / | S |。 > 0.001。 当应用程序遇到密度较低(小于0.1%)的整数集时,位图不太可能是正确的数据结构。

提出的Roaring数据布局特意简单。 几种变化是可能的。 对于非常密集的位图,当每个容器中有超过2 16-4096个整数时,我们可以存储零位的位置,而不是2 16位位图。 此外,我们可以更好地压缩连续整数的序列。 我们将对这些可能性的调查留作未来的工作。

3. 访问操作

为了检查是否存在32位整数x,我们首先使用二进制搜索来查找与x / 2 16对应的容器。 如果找到位图容器,则访问第(x mod 2 16)位。 如果找到了数组容器,我们将再次使用二进制搜索。

我们类似地插入和删除整数x。 我们首先寻找相应的容器。 当找到的容器是位图时,我们设置相应位的值并相应地更新基数。 如果找到数组容器,则使用二进制搜索,然后进行线性时间插入或删除。

删除整数时,如果基数容器的基数达到4096,则位图容器可能会成为数组容器。添加整数时,当基数容器的基数超过4096时,数组容器可能会成为位图容器。当发生这种情况时,将使用更新后的新容器创建 旧容器被丢弃时的数据。 通过创建一个用零初始化的新位图容器并设置相应的位,可以将数组容器转换为位图容器。 要将位图容器转换为数组容器,我们使用优化的算法(请参见算法2)提取设置位的位置。

4.逻辑操作

我们对Roaring位图实施了各种操作,包括并集(按位“或”)和交集(按位“与”)。两个咆哮位图之间的按位操作包括迭代和比较第一级索引上的16个高位整数(键)。为了获得更好的性能,我们维护排序的第一级数组。每次迭代都会比较两个键。相等时,将在相应容器之间执行第二级逻辑操作。这总是生成一个新的容器。如果容器不为空,则将其与公用密钥一起添加到结果中。然后,位于第一级数组上的迭代器将增加1。当两个键不相等时,包含最小键的数组将增加一个位置,如果执行并集,则将最低键和相应容器的副本添加到答案中。在计算联合时,我们将重复执行直到两个第一级数组都用完为止。当计算交集时,我们会在一个数组用完后立即终止。

排序的第一级数组允许在O(n 1 + n 2)时间进行第一级比较,其中n 1和n 2是两个比较数组的各自长度。 我们还维护了具有相同优势的已排序数组容器。 由于容器可以用两种不同的数据结构(位图和数组)表示,因此两个容器之间的逻辑联合或交叉涉及以下场景:

位图与位图:

我们遍历1024个64位字。对于联合,我们执行1024个按位OR,然后将结果写入新的位图容器。 请参阅算法1。使用Long.bitCount方法在Java中有效地计算了所得基数。

  • 算法1,用于计算两个位图容器的并集。
1:输入:两个位图A和B索引为102464位整数的数组
2:输出:表示A和B的并集的位图C及其基数c
3: c ← 0
4: Let C be indexed as an array of 1024 64-bit integers
5: for i ∈ {1,2,...,1024} do
6: C i ← A i OR B i
7: c ← c + bitCount(C i )
8: return C and c

与仅计算按位OR相比,计算按位OR似乎要慢得多,而计算结果的基数似乎要慢得多。 但是,有四个因素可以缓解此潜在问题。

  1. 流行的处理器(Intel,AMD,ARM)具有快速的指令来计算一个单词的数量。 英特尔和AMD的popcnt指令的吞吐量高达每个CPU周期一个操作。
  2. 最近的Java实现将对Long.bitCount的调用转换为如此快速的指令。
  3. 流行的处理器是超标量的:它们可以一次执行多个操作。 因此,当我们检索下一个数据元素时,计算它们的按位“或”并将其存储在内存中,处理器可以在最后的结果上应用流行的指令,并相应地增加基数计数器。
  4. 对于廉价的数据处理操作,由于高速缓存未命中,处理器可能无法满负荷运行。

在用于实验的Java平台上,我们估计可以以每秒7亿个64位字的速度计算和编写按位OR。 如果我们在生成结果时进一步计算结果的基数,则估计速度将降至每秒约5亿个单词。 也就是说,因为我们保持基数,所以我们遭受了大约30%的速度损失。 相反,诸如WAH和Concise之类的竞争方法在执行单个按位运算之前必须花费时间来解码单词类型。 这些检查可能会导致昂贵的分支错误预测或削弱超标量执行。

对于计算交叉路口,我们使用不太直接的路线。 首先,我们使用1024位AND指令计算结果的基数。 如果基数大于4096,则继续进行联合,将按位AND的结果写入新的位图容器。 否则,我们将创建一个新的数组容器。 我们使用算法2即时从按位AND中提取设置的位。请参见算法3。

算法2优化算法,可将位图中的设置位转换为整数列表。 我们假设二补数的算术。 函数bitCount返回整数的汉明权重。

  • 算法2优化算法,可将位图中的设置位转换为整数列表。 我们假设二补数的算术。 函数bitCount返回整数的汉明权重。
输入:一个整数w
输出:包含索引的数组S,在w中可以找到13: Let S be an initially empty list
4: while w 6= 0 do
5: t ← w AND − w (cf. [12, p. 12])
6: append bitCount( t − 1 ) to S
7: w ← w AND (w − 1) (cf. [12, p. 11])
8: return S

(frank 这部分应该与汉明权重,二补算法有关的吧)

位图与数组:

当两个容器中的一个位图与另一个人中的一个排序后的动态数组时,可以非常快速地计算出交集:我们遍历排序后的动态数组,并验证位图容器中每个16位整数的存在。 结果被写到数组容器中。 联合也是有效的:我们创建位图的副本,然后简单地遍历数组,设置相应的位。

数组与数组:

对于联合,如果基数之和不超过4096,则在两个数组之间使用合并算法。否则,我们在位图容器中设置与两个数组相对应的位。然后,我们使用快速指令来计算基数。如果基数不超过4096,我们将位图容器转换为数组容器(请参见算法2)。对于相交,当两个数组的基数相差小于64时,我们使用简单的合并(类似于在合并排序中完成的操作)。否则,我们使用疾驰的相交[8]。结果总是写到新的数组容器中。当一个数组(r)比另一个数组(f)小得多时,疾驰优于简单合并,因为它可以跳过许多比较。从两个数组的开头开始,我们从小数组r中选择下一个availableinri ri,并在大数组f中寻找一个至少与fj一样大的整数,首先看下一个值,然后看两倍的值,等等。然后,我们使用二进制搜索在第二个列表中前进到大于或等于r i的第一个值。

我们还可以执行其中一些操作:

  • 在计算两个位图容器之间的联合时,我们可以修改两个位图容器之一,而不用生成新的位图容器。 同样,对于两个位图容器之间的交集,如果结果的基数超过4096,则可以修改两个容器之一。
  • 在计算数组和位图容器之间的联合时,我们可以通过遍历数组容器的值并在位图容器中设置相应的位,将结果写入位图容器。 我们可以通过检查单词值是否已被修改来每次更新基数。

就地操作可以更快,因为它们避免分配和初始化新的内存区域。 当汇总许多位图时,我们使用其他优化方法。 例如,当计算许多位图(例如数百个)的并集时,我们首先找到所有具有相同密钥的容器(使用优先级队列)。 如果一个这样的容器是位图容器,那么我们可以克隆该位图容器(如果需要),并计算该容器与所有相应容器在位的并集。 在这种情况下,基数的计算可以最后进行一次。 参见算法4。

5. EXPERIMENTS

我们进行了一系列的实验,将Roaring位图的时空性能与其他著名的位图索引方案(Java s BitSet, WAH和Concise)的性能进行比较。我们使用了由Colantonio和Di Pietro[1]提供的用于WAH和CONCISE(2.2版本)的Concise Java库。

我们的Roaring-bitmap实现的代码和数据在http://roaringbitmap.org/上免费提供。我们的软件经过了完全的测试,我们的Java库已经被主流数据库系统如Apache Spark[13]和Druid[14]采用。基准测试是在AMD FXTM-8150八核处理器上执行的,运行频率为3.60 GHz,内存为16 GB。我们在Linux Ubuntu 12.04.1 LTS上使用Oracle服务器JVM version 1.7。我们所有的实验都是在内存中进行的。

(frank 算法4 还没补)

为了说明Java中的即时编译器,我们首先运行测试而不记录时间。
然后,我们重复测试几次并报告平均值。

5.1 综合实验

我们开始通过复制Colantonio和DiPietro的综合实验[1]。但是,尽管它们包括Java HashSet等替代数据结构,但为简化起见,我们仅关注位图格式。 考虑到我们拥有更好的处理器,我们的结果通常与Colantonio和Di Pietro的结果一致。

根据两个合成数据分布生成了10 ^ 5整数的数据集:均匀和离散的Beta(0.5,1)分布。(Colantonio和Di Pietro将后者描述为Zipfian分布。)在四种密度d(从2 ^ -10到0.5)变化的情况下比较了这四个方案。为了生成整数,我们首先在[0,1)中伪随机地选择了一个浮点数y。当需要均匀分布时,我们将×maxc添加到集合中。在β分布的情况下,我们添加了[y ^ 2×max]。最大值max表示要生成的整数总数与集合的所需密度(d)之比,即:max = 105 / d。由于均匀分布和Beta(0.5,1)分布的结果通常相似,因此我们没有系统地介绍这两者。

我们强调,我们的数据分布和基准测试紧随Colantonio和Di Pietro的工作[1]。由于他们使用此基准来显示Concise优于WAH的优势,因此,我们认为使用自己的基准来评估针对Concise的建议是公平的。图2a和2b显示了Java的BitSet使用的平均位数以及三种将位图存储在集合中的位图压缩技术。在这些测试中,Roaring位图需要Concise占用50%的空间,而稀疏位图需要25%的WAH空间。即使在我们的测试中,即使对于密集位图,BitSet类也会使用更多的内存。这是由于其内存分配策略可在存储量增加时使基础数组的大小增加一倍是必须的。我们可以通过克隆新构建的位图来恢复浪费的内存。我们Roaring的位图实现具有trim方法,可用于获得相同的结果。在这些测试中,我们没有调用这些方法。我们还报告交集和并集时间。也就是说,我们获取两个位图,并生成一个表示相交或并集的新位图。对于BitSet,这意味着我们首先需要创建一个副本(使用clone方法),因为按位操作是就地的。图2c和2d表示平均时间(以纳秒为单位),以执行两组整数之间的相交和并集。对于所有测试密度的交集,Roaring位图的速度是Concise和WAH的×4 −×5倍。并集的结果相似,除了中等密度(2 ^ -5≤d≤2 ^ -4)之外,Roaring仅比Concise和WAH中等(30%)。在密集数据上,BitSet的性能优于其他方案,但在稀疏位图上,BitSet的速度要慢10倍以上。我们测量了每种方案将单个元素a添加到整数排序集合S中所需的时间,即:∀i∈S:a> i。图2e显示,Roaring所需的时间少于WAH和Concise。此外,与Roaring位图不同,WAH和Concise不支持以随机顺序有效地插入值。最后,我们测量了从一个随机选择元素中删除一个随机选择的元素所需的时间整数集(图2f)。我们观察到Roaring位图比其他两种压缩格式具有更好的结果。

5.2 真实数据实验

表I II显示了五个真实数据集的结果,这些数据集使用了早期对压缩位图索引[15]的研究。只有两个例外:

•我们只将1985年9月的数据用于天气数据集(其他人在[16]之前使用过的一种方法),否则它对于我们的测试环境来说太大了。•我们省略了CENSUS2000数据集,因为它只包含在一个大宇宙(n = 37 019 068)中平均基数为30的位图。对于位图来说,这是一个不适合的场景。由于结构开销,Roaring bitmap使用的内存和Concise位图一样多。然而,当计算交集时,Roaring位图要快4倍。

数据集是按原样获取的:在建立索引之前,我们没有对它们排序。

对于每个数据集,都建立了位图索引。然后,我们从索引中选择200位图,使用类似于分层抽样的方法来控制属性基数的大范围。我们首先抽样200个属性,并进行替换。对于每个采样的属性,我们随机选择其中一个位图。使用200位图作为100对输入的100对AND和OR操作;表IIb和IIc显示了当Roaring被BitSet, WAH或Concise取代时,时间因子增加。(值低于1.0表示Roaring速度较慢。)表IIa显示了当Roaring被其他方法之一取代时存储因子的增加。

一般来说,Roaring bitmap总是比WAH和Concise更快。在两个数据集(CENSUS1881和WIKILEAKS)上,Roaring bitmap比BitSet快,同时使用更少的内存(少40个)。在另外两个数据集上,BitSet的速度是Roaring bitmap的两倍多,但它也使用了三倍的内存。当比较BitSet和Roaring bitmap的速度时,可以考虑Roaring bitmap预先计算块级别的基数。因此,如果我们需要聚合位图的基数,Roaring bitmap就有优势。在WIKILEAKS的数据集上,Concise和WAH提供了比Roaring更好的压缩(大约30%)。这是由于存在一个长时间的运行(11···1填充词),Roaring bitmap不会压缩。

CENSUS1881数据集的结果是惊人的:Roaring比其他方法快了900倍。这是由于位图的基数有很大的差异。当稀疏的位图与稠密的位图取交集时,Roaring是特别有效的。

6. 结论

本文介绍了一种新的位图压缩方案——Roaring。它将位图集项存储为32位整数,存储在一个空间效率高的两级索引中。与两种有竞争力的位图压缩方案相比,WAH和Concise,Roaring通常使用更少的内存和更快。当数据是有序的,位图需要存储长期连续的值(例如,在WIKILEAKS数据集),替代方案,如Concise或WAH可能提供更好的压缩比。然而,即使在这些情况下,Roaring也可能更快。在未来的工作中,我们将考虑进一步提高性能和压缩比。我们可以添加新类型的容器。特别是,我们可以使用快速封装技术来优化数组容器[17]的存储使用。我们可以在算法中使用SIMD指令[18,19,20]。我们还应该回顾除交集和并集之外的其他操作,比如阈值查询[21]。我们计划进一步研究在信息检索方面的应用。我们有理由感到乐观:Apache Lucene(从5.0版开始)已经采用了一种Roaring格式[22]来压缩文档标识符。

还没做完的事

  • concise的原理?
  • 用于测试的数据,论文中说的百倍速?
  • popcnt
  • 介绍那块的描述,感觉生硬
  • 每秒7亿个64位字的速度
  • 疾驰优于简单合并

(末完待续)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值