Roaring Bitmap的简介

1,608 阅读6分钟

概览

在本教程中,我们将学习关于Roaring Bitmap的知识。我们将使用一些基本操作作为Roaring Bitmap的示例。此外,我们还将在Java中执行RoaringBitmap和BitSet之间的性能测试。

Roaring Bitmap 简介

  • Roaring Bitmap的数据结构因其高性能和压缩比而常用于分析、搜索和大数据项目。它的设计灵感来自于位图索引,一种有效表示数字数组的数据结构。它类似于Java BitSet,但具有压缩功能。
  • 在保持对单个元素快速访问的同时压缩大整数集合是 Roaring Bitmap 的一个重要优势。Roaring Bitmap 内部使用不同类型的容器来实现这一点。

Roaring Bitmap是如何工作的

一组Roaring Bitmap是由不相交子集容器组成的无符号整数集。每个子集都有一个16位键索引,并且可以容纳大小为2^16的值范围。这使得无符号32位整数可以存储为Java shorts,因为最坏情况下只需要16位来表示单个32位值。

image.png

整数的16个最高有效位是桶或块键。每个数据块表示区间(0<= n < 2^16)中值范围的基础。此外,如果值范围内没有数据,则不会创建该数据块。

下图是具有不同数据的咆哮位图示例:

image.png

在第一个块中,我们存储了 2 的前 10 个倍数。此外,在第二个块中,我们有 100 个从 65536 开始的连续整数。图像中的最后一个块具有 131072 到 19660 之间的偶数。

Roaring Bitmap的容器

Roaring Bitmap中的容器主要分为三种——数组、位图和运行容器。根据分区集的特性,Run Container、Bitmap Container 或 Array Container 是保存分区数据的容器的实现。

当我们向 roaring bitmap 添加数据时,在内部,它会根据值是否适合容器键覆盖的范围来决定是创建一个新容器还是更改现有容器。

数组容器

  • Array Container 不压缩数据,只保存少量数据。它占用的空间量与其保存的数据量成正比:每个都是两个字节。
  • Array Container使用的数据类型是short。整数按排序顺序存储。
  • 此外,数组的初始容量为4,最大元素数量为4096。数组容量是动态变化的。然而,当元素数量超过4096时,Roaring Bitmap会将Array Container内部转换为Bitmap Container。
  • 让我们看一个在 Roaring Bitmap 中向数组容器插入数据的例子。我们有数字 131090。最高的16位是0000 0000 0000 0010,这是我们的键值。低16位是0000 0000 0001 0010。当我们将其转换为十进制时,它的值为18。现在,在插入数据后,这就是我们 Roaring Bitmap 的结构:

image.png

我们可以注意到,插入后,Array Container 的基数对于最高 16 位代表的键为 5。

Roaring Bitmap容器

  • Bitmap Container 是 bitset 的经典实现。
  • 使用长数组来存储位图数据。该数组容量恒定为1024,不像动态扩展数组的Array Container。Bitmap Container不需要查找位置,而是直接访问索引。
  • 为了观察它的工作原理,我们将使用一个简单的例子。我们将把数字32786插入到Roaring位图中。前16位是0000 0000 0000 0000。其余的位是1000 0000 0001 0010或者在十进制表示中为32786。这个值代表要在位图容器中设置的索引。让我们看看带有新信息的Roaring位图:

image.png

RoaringBitmap 的运行容器

  • Run-length Encoding(RLE)是位图中某个区域有大量干净单词时的最佳容器选择。它使用短数据类型数组。
  • 偶数索引处的值表示运行的开始,奇数索引处的值表示这些运行的长度。通过遍历完整的运行数组来计算容器的基数。
  • 例如,下图向我们展示了一个包含连续整数序列的容器。然后,在 RLE 执行之后,容器只有四个值:

image.png

这些值表示为11后跟随四个连续递增的值,以及27后跟随两个连续递增的值。

这个压缩算法的工作方式取决于数据的紧凑程度或连续性。如果我们有100个短整数都在一排,它可以将它们从200字节压缩到4字节,但如果它们分散在不同位置,编码后大小会从200字节变为400字节。

RoaringBitmap中的联合

在我们开始代码示例之前,让我们简要介绍一下 Roaring Bitmap,并将 RoaringBitmap 依赖添加到我们的 pom.xml 文件中:

<dependency>
    <groupId>org.roaringbitmap</groupId>
    <artifactId>RoaringBitmap</artifactId>
    <version>0.9.38</version>
</dependency>

集合的并集是测试RoaringBitmap的第一个操作。首先,让我们声明两个 RoaringBitmap 实例。第一个是A,第二个是B:

@Test
void givenTwoRoaringBitmap_whenUsingOr_thenWillGetSetsUnion() {
    RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3, 4, 5, 6, 7, 8);
    RoaringBitmap A = RoaringBitmap.bitmapOf(1, 2, 3, 4, 5);
    RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
    RoaringBitmap union = RoaringBitmap.or(A, B);
    assertEquals(expected, union);
}

在上面的代码中,我们声明了两个RoaringBitmap实例。我们使用RoaringBitmap提供的bitmapOf()静态工厂方法来创建实例。然后,我们使用or()方法执行集合并操作。在幕后,此操作在设置位图之间完成逻辑OR运算。这是一个线程安全的操作。

RoaringBitmap 中的交集

让我们针对交集问题实施我们的测试用例。与联合一样,交集操作在 RoaringBitmap 中非常简单:

@Test
void givenTwoRoaringBitmap_whenUsingAnd_thenWillGetSetsIntersection() {
    RoaringBitmap expected = RoaringBitmap.bitmapOf(4, 5);
    RoaringBitmap A = RoaringBitmap.bitmapOfRange(1, 6);
    RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
    RoaringBitmap intersection = RoaringBitmap.and(A, B);
    assertEquals(expected, intersection);
}
  • 我们使用RoaringBitmap类中的另一个静态工厂方法声明了这个测试用例中的A集合。bitmapOfRange()静态方法创建了一个新的RoaringBitmap。在底层,bitmapOfRange()方法创建了一个新实例,并使用add()方法将数据添加到Roaring Bitmap范围内。在本例中,add()方法接收两个长整型值作为参数,表示下限和上限。下限是包含的,而上限则不包含在结果集范围内。对于当前API版本,接收两个int值作为参数的add()方法已被弃用。
  • 然后,我们使用and()方法执行交集操作。正如其名称所示,and()方法在两个集合之间执行逻辑AND操作。此操作是线程安全的。

RoaringBitmap的差集

除了并集和交集,我们还有集合的相对补运算。 接下来,让我们用 RoaringBitmap 构建我们的设置差异测试用例:

@Test
void givenTwoRoaringBitmap_whenUsingAndNot_thenWillGetSetsDifference() {
    RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3);
    RoaringBitmap A = new RoaringBitmap();
    A.add(1L, 6L);
    RoaringBitmap B = RoaringBitmap.bitmapOf(4, 5, 6, 7, 8);
    RoaringBitmap difference = RoaringBitmap.andNot(A, B);
    assertEquals(expected, difference);
}
  • 像我们之前的代码示例一样,我们声明了两个集合A和B。对于这种情况,我们使用不同的方法来实例化A集合。首先创建一个空的RoaringBitmap,然后使用add()方法添加元素,与上一节中描述的bitmapOfRange()方法相同。
  • andNot()方法执行的是A和B的集合差,从逻辑上看,这个操作执行的是按位ANDNOT(差)运算。只要给定的位图保持不变,此操作就是线程安全的。

RoaringBitmap的异或运算

此外,Roaring Bitmap 中还有 XOR(异或)操作。该操作类似于集合的相对补集,但是两个集合之间的共同元素将从结果集中省略。 我们使用xor()方法来执行此操作。让我们进入我们的测试代码示例:

@Test
void givenTwoRoaringBitmap_whenUsingXOR_thenWillGetSetsSymmetricDifference() {
    RoaringBitmap expected = RoaringBitmap.bitmapOf(1, 2, 3, 6, 7, 8);
    RoaringBitmap A = RoaringBitmap.bitmapOfRange(1, 6);
    RoaringBitmap B = RoaringBitmap.bitmapOfRange(4, 9);
    RoaringBitmap xor = RoaringBitmap.xor(A, B);
    assertEquals(expected, xor);
}

简而言之,RoaringBitmap类中的xor()方法执行的是按位异或运算,是线程安全的。

和BitSet对比下性能

此外,让我们在 RoaringBitmap 和 Java BitSet 之间构建一个简单的性能测试。对于每个集合类型,我们测试前面描述的操作:联合、交集、差异和异或。

我们使用 Java Microbenchmark Harness (JMH) 来实施我们的性能测试。首先,我们需要将依赖项添加到我们的 pom.xml 文件中:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.36</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.36</version>
</dependency>

声明基准范围

接下来,我们在为测试设置初始条件时声明我们的类和集合:

@State(Scope.Thread)
class BitSetsBenchmark {
    private RoaringBitmap rb1;
    private BitSet bs1;
    private RoaringBitmap rb2;
    private BitSet bs2;
    private final static int SIZE = 10_000_000;
}

最初,我们为每种类型声明了两组,BitSet 和 RoaringBitmap。然后,我们设置一个最大size。 SIZE 变量是我们将用作集合大小的上限。

我们将在本课程中执行所有测试。此外,我们使用 Scope.Thread 作为 State 和默认的吞吐量基准模式

我们将在操作后生成一个新集合,以避免改变我们的输入数据结构。避免突变对于并发上下文很重要。这就是为什么,对于 BitSet 案例,我们将克隆输入集,这样结果数据就不会改变输入集。

基准数据设置

接下来,让我们为测试设置数据:

@Setup
public void setup() {
    rb1 = new RoaringBitmap();
    bs1 = new BitSet(SIZE);
    rb2 = new RoaringBitmap();
    bs2 = new BitSet(SIZE);
    for (int i = 0; i < SIZE / 2; i++) {
        rb1.add(i);
        bs1.set(i);
    }
    for (int i = SIZE / 2; i < SIZE; i++) {
        rb2.add(i);
        bs2.set(i);
    }
}

我们的两个 BitSet 集合被初始化为 SIZE。然后,对于第一个 RoaringBitmap 和 BitSet,我们将值添加/设置为 SIZE / 2,不包括在内。对于其他两组,我们将 SIZE / 2 的值添加到 SIZE。

基准测试

最后,让我们编写测试代码。让我们从联合操作开始:

@Benchmark
public RoaringBitmap roaringBitmapUnion() {
    return RoaringBitmap.or(rb1, rb2);
}
@Benchmark
public BitSet bitSetUnion() {
    BitSet result = (BitSet) bs1.clone();
    result.or(bs2);
    return result;
}

第二个操作是交集:

@Benchmark
public RoaringBitmap roaringBitmapIntersection() {
    return RoaringBitmap.and(rb1, rb2);
}
@Benchmark
public BitSet bitSetIntersection() {
    BitSet result = (BitSet) bs1.clone();
    result.and(bs2);
    return result;
}

第三个是区别:

@Benchmark
public RoaringBitmap roaringBitmapDifference() {
    return RoaringBitmap.andNot(rb1, rb2);
}
@Benchmark
public BitSet bitSetDifference() {
    BitSet result = (BitSet) bs1.clone();
    result.andNot(bs2);
    return result;
}

最后一个是异或运算:

@Benchmark
public RoaringBitmap roaringBitmapXOR() {
    return RoaringBitmap.xor(rb1, rb2);
}
@Benchmark
public BitSet bitSetXOR() {
    BitSet result = (BitSet) bs1.clone();
    result.xor(bs2);
    return result;
}

基准测试结果

执行我们的基准测试后,我们得到以下结果:

Benchmark                                    Mode  Cnt       Score       Error  Units
BitSetsBenchmark.bitSetDifference           thrpt   25    3890.694 ±   313.808  ops/s
BitSetsBenchmark.bitSetIntersection         thrpt   25    3542.387 ±   296.007  ops/s
BitSetsBenchmark.bitSetUnion                thrpt   25    3012.666 ±   503.821  ops/s
BitSetsBenchmark.bitSetXOR                  thrpt   25    2872.402 ±   348.099  ops/s
BitSetsBenchmark.roaringBitmapDifference    thrpt   25   12930.064 ±   527.289  ops/s
BitSetsBenchmark.roaringBitmapIntersection  thrpt   25  824167.502 ± 30176.431  ops/s
BitSetsBenchmark.roaringBitmapUnion         thrpt   25    6287.477 ±   250.657  ops/s
BitSetsBenchmark.roaringBitmapXOR           thrpt   25    6060.993 ±   607.562  ops/s

我们可以注意到,RoaringBitmap 比 BitSet 操作执行得更好。尽管有这些结果,但我们需要考虑何时使用每种类型。 在某些情况下尝试使用压缩位图是一种浪费。例如,这可能是我们有一个小数据世界的时候。如果我们可以在不增加内存使用的情况下解压缩 BitSet,那么压缩位图可能不适合我们。如果不需要压缩,BitSet 可提供出色的速度。

结论

在本文中,我们了解了咆哮的位图数据结构。我们讨论了 RoaringBitmap 的一些操作。此外,我们在 RoaringBitmap 和 BitSet 之间进行了一些性能测试。结果,我们了解到前者的表现优于后者。

英文地址:

www.baeldung.com/java-roarin…