RoaringBitmap源码分析一(AND操作)

在上文“RoaringBitmap简析”中,简单的描述了RoaringBitmap的原理,主要是关于底层的数据结构。今天再重点根据源码来分析常用的位图操作是如何高效实现的。 考虑到Bitmap在搜索引擎和列存储中的使用场景,AND应该是最常用的操作,我们来重点分析。

首先,我们知道在Roaring Bitmap中,一个bitmap包含一个RoaringArray类型的成员变量highLowContainer,用于存储数据。 RoaringArray包含两个数组,分别是short[] keys和Container[] values。keys中的元素用来标记对应的values中的container表示哪个区间的整数。Container有三个子类,分别是ArrayContainer,BitmapContainer,和RunContainer。当一个区间中有不超过4096个元素的时候,使用Array形式;当大于4096的时候,使用Bitmap形式。RunContainer是基于RLE的方式,先跳过。

其次,只有相同的key所对应的value才有交集的可能。如果key不同,则不可能有交集,因为所代表的区间不同。

第三,一个ArrayContainer无论是和ArrayContainer还是和BitmapContainer作交集,结果都一定还是ArrayContainer。两个BitmapContainer作交集,结果即可能是BitmapContainer,也可能是ArrayContainer。

我们先看看BitmapContainer的and(ArrayContainer)方法:

  @Override
  public ArrayContainer and(final ArrayContainer value2) {
    final ArrayContainer answer = new ArrayContainer(value2.content.length);
    int c = value2.cardinality;
    for (int k = 0; k < c; ++k) {
      short v = value2.content[k];
      if (this.contains(v)) {
        answer.content[answer.cardinality++] = v;
      }
    }
    return answer;
  }

首先,声明一个ArrayContainer类型的变量answer,初始长度为ArrayContainer的content的长度,这也是理论上的结果集中元素个数的上限。然后简单的使用一个for循环,依次判断ArrayContainer中的元素是否在BitmapContainer中存在,如果存在的话,则更新到结果answer中,并增加cardinality。最终,循环结束,返回answer。ArrayContainer中可能存在内存开销浪费的情况,但考虑到性能,这样的浪费是可以接受的。

再来看看BitmapContainer的and(BitmapContainer)方法:

  @Override
  public Container and(final BitmapContainer value2) {
    int newCardinality = 0;
    for (int k = 0; k < this.bitmap.length; ++k) {
      newCardinality += Long.bitCount(this.bitmap[k] & value2.bitmap[k]);
    }
    if (newCardinality > ArrayContainer.DEFAULT_MAX_SIZE) {
      final BitmapContainer answer = new BitmapContainer();
      for (int k = 0; k < answer.bitmap.length; ++k) {
        answer.bitmap[k] = this.bitmap[k] & value2.bitmap[k];
      }
      answer.cardinality = newCardinality;
      return answer;
    }
    ArrayContainer ac = new ArrayContainer(newCardinality);
    Util.fillArrayAND(ac.content, this.bitmap, value2.bitmap);
    ac.cardinality = newCardinality;
    return ac;
  }

首先需要计算两个BitmapContainer的交集的cardinality。此处用到了一个很精妙的代码,即Long.bitCount()。如果结果集的cardinality大于4096,则返回结果为BitmapContainer。如果cardinality小于等于4096,则使用ArrayContainer来存储结果。此处用到了Util.fillArrayAND来取两个BitmapContainer的交集。

  public static void fillArrayAND(final short[] container, final long[] bitmap1,
      final long[] bitmap2) {
    int pos = 0;
    if (bitmap1.length != bitmap2.length) {
      throw new IllegalArgumentException("not supported");
    }
    for (int k = 0; k < bitmap1.length; ++k) {
      long bitset = bitmap1[k] & bitmap2[k];
      while (bitset != 0) {
        long t = bitset & -bitset;
        container[pos++] = (short) (k * 64 + Long.bitCount(t - 1));
        bitset ^= t;
      }
    }
  }

从while循环,以及&和^等操作,可以感觉到和计算一个整数的二进制表示中1的个数的算法有接近之处。实际使用几个例子带入,就能明白此处是在快速计算bitset中1的位置所代表的数字。复杂度为二进制中1的位数。

ArrayContainer的and(BitmapContainer)的代码实现非常简单:

 @Override
  public Container and(BitmapContainer x) {
    return x.and(this);
  }

实际就是调用了BitmapContainer的and(ArrayContainer)方法。

最后,是ArrayContainer的and(ArrayContainer)方法。

  @Override
  public ArrayContainer and(final ArrayContainer value2) {
    ArrayContainer value1 = this;
    final int desiredCapacity = Math.min(value1.getCardinality(), value2.getCardinality());
    ArrayContainer answer = new ArrayContainer(desiredCapacity);
    answer.cardinality = Util.unsignedIntersect2by2(value1.content, value1.getCardinality(),
        value2.content, value2.getCardinality(), answer.content);
    return answer;
  }

首先计算结果集的cardinality的上限,并初始化。然后调用Util.unsignedIntersect2by2来intersect两个ArrayContainer。而这个方法也无所不用其极的展示了性能优化的小trick。

  public static int unsignedIntersect2by2(final short[] set1, final int length1, final short[] set2,
      final int length2, final short[] buffer) {
    if (set1.length * 64 < set2.length) {
      return unsignedOneSidedGallopingIntersect2by2(set1, length1, set2, length2, buffer);
    } else if (set2.length * 64 < set1.length) {
      return unsignedOneSidedGallopingIntersect2by2(set2, length2, set1, length1, buffer);
    } else {
      return unsignedLocalIntersect2by2(set1, length1, set2, length2, buffer);
    }
  }

当两个ArrayContainer中的元素个数差距不大的时候,使用unsignedLocalIntersect2by2,步长为1的进行比较;而当个数差距很大的时候(此处为64倍),则使用unsignedOneSidedGallopingIntersect2by2,步长为2的幂次方的形式递增,可以加速跳过不相交的元素。直观感觉64应该是一个拍脑袋的数字,可能还有更好的选择。

上面就是AND操作的几个重点的方法。代码中还有iand方法,代表in-place的AND操作,和非in-place的代码原理是一样的,此处也略过了。

你可能感兴趣的:(RoaringBitmap源码分析一(AND操作))