在上文“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的代码原理是一样的,此处也略过了。