什么时候使用bitmap
集合是软件中的基本抽象。它们可以以各种方式实现,例如哈希集,树等。在数据库和搜索引擎中,集合通常是索引的组成部分。例如,我们可能需要维护一组满足某些属性的所有文档或行(由数字标识符表示)。除了从集合中添加或删除元素外,我们还需要快速函数来计算交集,并集,集合之间的差等。
要实现一组整数,一种特别吸引人的策略是位图(也称为位集或位向量)。使用n位,我们可以表示由[0,n)范围内的整数组成的任何集合:如果整数i存在于集合中,则将第i位设置为1。商品处理器使用W = 32或W = 64位的字。通过组合许多这样的单词,我们可以支持较大的n值。交集,并集和差异然后可以实现为按位AND,OR和ANDNOT运算。更复杂的集合函数也可以实现为按位运算。
当比特集方法适用时,它可以比集合的其他可能实现(例如,作为散列集合)快几个数量级,同时使用更少的存储器。
但是,位集甚至压缩位并不总是适用。例如,如果您有1000个看似随机的整数,那么一个简单的数组可能是最好的表示形式。我们将这种情况称为“稀疏”方案。
什么时候使用压缩的bitmap
未压缩的BitSet会占用大量内存。例如,如果使用BitSet并将位置1,000,000的位设置为true,则刚好超过100kB。存储一个位的位置超过100kB。即使您不关心内存,这也是浪费的:假定您需要计算此BitSet与另一个在位置1,000,001处具有真值的真位之间的交集,那么您需要遍历所有这些零,无论您是否喜欢它或不。这可能变得非常浪费。
话虽如此,在某些情况下,尝试使用压缩的位图肯定是浪费的。例如,如果您的Universe尺寸较小。例如,您的位图表示[0,n)中的整数集,其中n较小(例如,n = 64或n = 128)。如果您能够解压缩BitSet并且不会消耗您的内存,那么压缩的位图可能对您没有用。实际上,如果您不需要压缩,则BitSet可以提供惊人的速度。
稀疏方案是不应该使用压缩位图的另一个用例。请记住,看起来随机的数据通常不可压缩。例如,如果您有一小组32位随机整数,则从数学上讲,每个整数使用少于32位是不可能的,并且尝试压缩可能会适得其反。
RoaringBitMap与其他bitmap的区别?
Roaring的大多数替代方法是较大的压缩位图家族的一部分,这些压缩位图是游程长度编码的位图。它们标识长期运行的1或0,并用标记词表示它们。如果本地混合使用1和0,则使用未压缩的单词。
这个家庭有很多格式:
Oracle的BBC在这一点上是一种过时的格式:尽管它可以提供良好的压缩,但是由于分支过多,它可能比最近的替代方法慢得多。
WAH是BBC的专利变体,可提供更好的性能。
Concise是对已获专利的WAH的改进。在某些特定情况下,它的压缩效果比WAH更好(最高2倍),但通常更慢。
EWAH都没有专利,而且比上述所有方法都快。不利的一面是,压缩效果不佳。它之所以更快,是因为它允许在未压缩的单词上进行某种形式的“跳过”。因此,尽管这些格式都不适合随机访问,但是EWAH比其他格式要好。
这些格式存在一个大问题,但是在某些情况下可能会严重伤害您:没有随机访问权限。如果要检查集合中是否存在给定值,则必须从头开始并“解压缩”整个内容。这意味着如果您想将一个大集合与一个大集合相交,那么在最坏的情况下,您仍然必须解压缩整个大集合...
Roaring解决了这个问题。它以以下方式工作。它将数据划分为216个整数的块(例如[0,216),[216,2 x 216),...)。在块内,它可以使用未压缩的位图,简单的整数列表或运行列表。无论使用哪种格式,它们都可以让您快速检查任何一个值的存在(例如,使用二进制搜索)。最终结果是Roaring可以比WAH,EWAH,Concise等游程长度编码格式更快地计算许多运算,也许令人惊讶的是,Roaring通常还提供更好的压缩率。
Code sample
import org.roaringbitmap.RoaringBitmap;
public class Basic {
public static void main(String[] args) {
RoaringBitmap rr = RoaringBitmap.bitmapOf(1,2,3,1000);
RoaringBitmap rr2 = new RoaringBitmap();
rr2.add(4000L,4255L);
rr.select(3); // would return the third value or 1000
rr.rank(2); // would return the rank of 2, which is index 1
rr.contains(1000); // will return true
rr.contains(7); // will return false
RoaringBitmap rror = RoaringBitmap.or(rr, rr2);// new bitmap
rr.or(rr2); //in-place computation
boolean equals = rror.equals(rr);// true
if(!equals) throw new RuntimeException("bug");
// number of values stored?
long cardinality = rr.getLongCardinality();
System.out.println(cardinality);
// a "forEach" is faster than this loop, but a loop is possible:
for(int i : rr) {
System.out.println(i);
}
}
}
Working with memory-mapped bitmaps(使用内存映射位图)
如果要使位图位于内存映射文件中,则可以改用org.roaringbitmap.buffer包。它包含两个重要的类:ImmutableRoaringBitmap和MutableRoaringBitmap。 MutableRoaringBitmaps是从ImmutableRoaringBitmap派生的,因此您可以在恒定时间内将MutableRoaringBitmap转换(投射)为ImmutableRoaringBitmap。
不是MutableRoaringBitmap实例的ImmutableRoaringBitmap由ByteBuffer支持,这具有一些性能开销,但具有更大的灵活性,即数据可以驻留在任何地方(包括Java堆外部)。
有时您可能需要使用磁盘上的位图(ImmutableRoaringBitmap的实例)和Java内存中的位图。如果您知道位图将驻留在Java内存中,那么最好使用MutableRoaringBitmap实例,不仅可以对其进行修改,而且还可以更快。此外,由于MutableRoaringBitmap实例也是ImmutableRoaringBitmap实例,因此您可以编写许多代码,期望使用ImmutableRoaringBitmap。
如果您编写的代码期望使用ImmutableRoaringBitmap实例,而没有尝试强制转换实例,则您的对象将是真正不可变的。 MutableRoaringBitmap具有便捷方法(toImmutableRoaringBitmap),该方法可以简单地转换为ImmutableRoaringBitmap实例。从语言设计的角度来看,ImmutableRoaringBitmap类的实例仅在按照ImmutableRoaringBitmap类的接口使用时才是不可变的。鉴于该类不是最终类,可以通过其他接口修改实例。因此,我们并不是以纯粹的方式来理解“不变”,而是以一种实际的方式。
我们将MutableRoaringBitmap实例转换为ImmutableRoaringBitmap实例的设计的动机之一是,位图通常很大,或者在避免内存分配的环境中使用,因此避免了强制复制。如果需要混合并匹配ImmutableRoaringBitmap和MutableRoaringBitmap实例,则可以预期会有副本。
下面的代码示例说明了如何从ByteBuffer创建ImmutableRoaringBitmap。在这种情况下,构造函数仅将元数据加载到RAM中,而按需从ByteBuffer中访问实际数据。
import org.roaringbitmap.buffer.*;
//...
MutableRoaringBitmap rr1 = MutableRoaringBitmap.bitmapOf(1, 2, 3, 1000);
MutableRoaringBitmap rr2 = MutableRoaringBitmap.bitmapOf( 2, 3, 1010);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
// If there were runs of consecutive values, you could
// call rr1.runOptimize(); or rr2.runOptimize(); to improve compression
rr1.serialize(dos);
rr2.serialize(dos);
dos.close();
ByteBuffer bb = ByteBuffer.wrap(bos.toByteArray());
ImmutableRoaringBitmap rrback1 = new ImmutableRoaringBitmap(bb);
bb.position(bb.position() + rrback1.serializedSizeInBytes());
ImmutableRoaringBitmap rrback2 = new ImmutableRoaringBitmap(bb);
另外,我们可以使用serialize(ByteBuffer)方法直接序列化为ByteBuffer。
对ImmutableRoaringBitmap进行的操作(例如x和,或xor翻转)将生成位于内存中的RoaringBitmap。 顾名思义,ImmutableRoaringBitmap本身无法修改。
这种设计的灵感来自德鲁伊。
您可以在测试文件TestMemoryMapping.java中找到一个完整的工作示例。
请注意,您不应将org.roaringbitmap包中的类与org.roaringbitmap.buffer包中的类混合使用。 它们不兼容。 但是,它们序列化为相同的输出。 org.roaringbitmap软件包中的代码的性能通常更高,因为使用ByteBuffer实例不会产生任何开销。
Kryo
Many applications use Kryo for serialization/deserialization. One can use Roaring bitmaps with Kryo efficiently thanks to a custom serializer (Kryo 5):
public class RoaringSerializer extends Serializer<RoaringBitmap> {
@Override
public void write(Kryo kryo, Output output, RoaringBitmap bitmap) {
try {
bitmap.serialize(new KryoDataOutput(output));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
@Override
public RoaringBitmap read(Kryo kryo, Input input, Class<? extends RoaringBitmap> type) {
RoaringBitmap bitmap = new RoaringBitmap();
try {
bitmap.deserialize(new KryoDataInput(input));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
return bitmap;
}
}
64-bit integers (long)
Though Roaring Bitmaps were designed with the 32-bit case in mind, we have an extension to 64-bit integers:
import org.roaringbitmap.longlong.*;
LongBitmapDataProvider r = Roaring64NavigableMap.bitmapOf(1,2,100,1000);
r.addLong(1234);
System.out.println(r.contains(1)); // true
System.out.println(r.contains(3)); // false
LongIterator i = r.getLongIterator();
while(i.hasNext()) System.out.println(i.next());
以上内容摘自RoaringBitMap的官网,方便查看就记录下来。以下是自己学习的心得:
1、kylin的bitmap是基于自有实现的Trie树的映射出来的id,这个可以追加的全局字典是kylin实现精确去重的第一步。
2、bitmap有很多实现,但是RoaringbitMap的实现是高效的,而这里边高效的原因是因为Roaring使用的container以及序列化,结合ByteBuffer,可以最大化bitmap的性能。而且Roaring里的子类,ImmutableRoaringBitmap以及内存映射MutableRoaringBitmap,都是高效使用bitmap的经典实现。
解释一下为什么这里用的 4096 这个阈值?因为一个 Integer 的低 16 位是 2Byte,因此对应到 Arrary Container 中的话就是 2Byte * 4096 = 8KB;同样,对于 Bitmap Container 来讲,2^16 个 bit 也相当于是 8KB。也可以理解,2Byte=16bit,2^16 = 4096*16,所以是4096这个值,是为了兼容存储Integer,如果全部数值是Long类型,可以支持的更长。Roaring有专门针对64位Long整形的实现。
这里总结一个例子吧,可以轻松地理解一下:
-
我们将 32-bit 的范围 ([0, n)) 划分为 2^16 个桶,每一个桶有一个 Container 来存放一个数值的低16位;
-
在存储和查询数值的时候,我们将一个数值 k 划分为高 16 位(k % 2^16)和低 16 位(k mod 2^16),取高 16 位找到对应的桶,然后在低 16 位存放在相应的 Container 中;
-
容器的话, RBM 使用两种容器结构: Array Container 和 Bitmap Container。Array Container 存放稀疏的数据,Bitmap Container 存放稠密的数据。即,若一个 Container 里面的 Integer 数量小于 4096,就用 Short 类型的有序数组来存储值。若大于 4096,就用 Bitmap 来存储值。
然后容器在一个RoaringBitmap中是可能混合存在的,要注意理解这一点。
关于bitmapContainer,记录一个小笔记: 这种Container使用long[]存储位图数据。我们知道,每个Container处理16位整形的数据,也就是0~65535,因此根据位图的原理,需要65536个比特来存储数据,每个比特位用1来表示有,0来表示无。每个long有64位,因此需要1024个long来提供65536个比特。
因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kb。
三种container的查询,bitmap的查询效率是恒定的,其余两种查询都是基于二分查找,这点对于效率影响还比较直接。
一般在调用runOptimize的时候,才会产生runContainer,因为这个container有很好的压缩算法。
后续会再挖一下Roaring的底层实现。怀挺!
参考:hexiaoqiao.github.io/blog/2016/1…
关于Roaring底层的原理,参考以下:
add原理:blog.csdn.net/chenfenggan…
container原理:blog.csdn.net/chenfenggan…
底层容器相互add过程:blog.csdn.net/chenfenggan…
序列化和反序化以及分布式应用:blog.csdn.net/chenfenggan…
针对druid讲解bitmap的使用:
针对kylin里bitmap的使用,参考:hexiaoqiao.github.io/blog/2017/0…