RoaringBitmap原理及在Go中的使用详解

目录
  • 引言
  • 1 什么是 RoaringBitmap
  • 2 数据结构
  • 3 三种 Container
    • 3.1 ArrayContainer
    • 3.2 BitmapContainer
    • 3.3 RunContainer
  • 4 Go 使用 RoaringBitmap
    • 4.1 并集运算
    • 4.2 交集运算
    • 4.3 差集运算
    • 4.4 异或运算
  • 5 总结

引言

今天我们聊聊 RoaringBitmap(咆哮位图)。在海量数据背景下,我们通常需要快速对数据计算、中间存储的需求。一系列专门为大数据准备的数据结构应运而生,常见的有 HyperLogLogBloomFilter等。

我们看一道老生常谈的面试题:

给定含有40亿个不重复的位于[0, 2^32 - 1]区间内的整数的集合,如何快速判定某个数是否在该集合内?

首先,40 亿在存储上我们需要消耗 40亿 * 32 位 = 160 Byte,大致是 16000 MB 即 14.9 GB 的内存,显然这是我们不能接受的。如果你给出的是这个答案,那么你就已经输了!

我们可以用位图来存储,第 0 个 bit 表示数字 0,第 1 个 Bit 表示数字 1,以此类推。如果某个数位于原集合内,就将它对应的位图内的 bit 置为 1,否则保持为 0。这样只占用了 512MB 的内存,不到原来的 3.4%。

我们会发现当数据稀疏的时候,也需要要开辟这么大的内存空间,就发挥不出其存储效率。为了解决位图不适应稀疏存储的问题,RoaringBitmap(咆哮位图)诞生了,因此本文重点探讨它。下面简称 RBM。

1 什么是 RoaringBitmap

是一种基于位图的数据结构,可以高效地存储大量的非负整数,并支持多种集合运算,如并集、交集、差集等。它可以高效地判断一个元素是否在集合中,并且可以使用很少的空间来存储集合。

2 数据结构

源码:

short[] keys;
Container[] values;
int size;

RoaringBitmap 当前有两个版本,分别用来存储 32 位和 64 位整数。以 32 位为例,RBM 会将 32 位的整形(int)拆分成高 16 位和低 16位 两部分来处理。其中

  • 高 16位 会被作为 key 存储到 short[] keys
  • 低 16 位则被看做 value,存储到 Container[] values中的某个 Container 中

keys 和 values 通过下标一一对应。size 则标示了当前包含的 key-value pair的数量,即 keys 和 values 中有效数据的数量。

注意:keys 数组永远保持有序,方便二分查找!

3 三种 Container

Container 是 RoaringBitmap的核心,我们结合上面的图会发现每个 32 位整形(int)的高 16 位已经作为key 存储在 RoaringArray 中了,那么 Container 只需要处理低 16 位的数据即可。

3.1 ArrayContainer

源码:

private static final int DEFAULT_INIT_SIZE = 4;
private static final int ARRAY_LAZY_LOWERBOUND = 1024;
static final int DEFAULT_MAX_SIZE = 4096;
private static final long serialVersionUID = 1L;
protected int cardinality;
short[] content;

从源码可以可以看出 16 位数据 value 直接存储在 short[] content中,因为是数组,始终保持顺序存储且不会重复,有利于二分查找。Container 存储数据没有任何压缩,只适合存储少量数据。

ArrayContainer 占用的空间大小与存储的数据量为线性关系,每个 short 大小为 2 kb,所以存储了 N 个数据的ArrayContainer 占用空间大致为 2N kb。存储一个数据需要占用 2kb,存储 4096 需要占用 8kb。

上面 DEFAULT_MAX_SIZE 值为 4096,可以知道,当容量超过这个值的时候会将当前 Container 替换为BitmapContainer。

3.2 BitmapContainer

源码:

private static final int DEFAULT_INIT_SIZE = 4;
private static final int ARRAY_LAZY_LOWERBOUND = 1024;
static final int DEFAULT_MAX_SIZE = 4096;
private static final long serialVersionUID = 1L;
protected int cardinality;
short[] content;

BitmapContainer 底层用了 long[] 存储位图数据。RMB 每个 Container处理 16 位整形(int)数据,0~65535,需要 65536 个 bit 来存储数据,每个 bit 位用 1 来表示有,0 来表示无。每个 long 有 64 位,所以需要 1024 个 long 来提供 65536 个 bit。

BitmapContainer 中无论存储了 1 个还是存储了 65536 个数据,其占用的空间都是同样的 8 kb (4096)。

3.3 RunContainer

源码:

private short[] valueslength;
int nbrruns;

RunContainer 又称行程长度压缩算法(Run Length Encoding),在连续数据上压缩效果显著。

RunContainer 原理在连续出现的数字,只会记录其初始数字和后续数量,举个例子:

  • 数列 22,它会压缩为 22,0;
  • 数列 22,23,24 它会压缩为 22,3;
  • 数列 22,23,24,32,33,它会压缩为 22,3,32,1;

其中,short[] valueslength中存储的就是压缩后的数据。

可以看出,这种压缩算法在性能和数据的连续性(紧凑性)关系极为密切,

  • 在连续的 100 个 short,可以将 200 字节压缩成 4 个 kb。
  • 对于不连续的 100 个 short,编码完之后会从 200 字节变为 400 kb。

如果要分析RunContainer的容量,我们可以做下面两种极端的假设:

  • 最优情况,只存在一个数据或者一串连续数字,存储 2 个 short 会占用 4 kb。
  • 最差情况,0~65535 的范围内填充所有的不连续数字,(全部奇数位或全部偶数位),需要存储 65536 个short 占用 128 kb。

小结一下:

4 Go 使用 RoaringBitmap

Go 语言支持了 RoaringBitmap,安装 roaring 库:

go get -u github.com/RoaringBitmap/roaring
// go get -u github.com/RoaringBitmap/roaring/roaring64

RoaringBitmap 支持多种集合运算,包括并集、交集、差集、异或等,这些运算都可以在高效地处理大规模数据集的同时,避免内存溢出和性能问题。

下面介绍一些 RoaringBitmap 集合运算的示例:

4.1 并集运算

// 创建两个 RoaringBitmap
rb1 := roaring.NewBitmap()
rb2 := roaring.NewBitmap()
// 添加元素
rb1.Add(1)
rb1.Add(2)
rb1.Add(3)
rb2.Add(3)
rb2.Add(4)
rb2.Add(5)
// 计算并集
rb3 := roaring.Or(rb1, rb2)
// 输出结果
fmt.Println(rb3.ToArray())
// Output: [1 2 3 4 5]

4.2 交集运算

// 创建两个 RoaringBitmap
rb1 := roaring.NewBitmap()
rb2 := roaring.NewBitmap()
// 添加元素
rb1.Add(1)
rb1.Add(2)
rb1.Add(3)
rb2.Add(3)
rb2.Add(4)
rb2.Add(5)
// 计算交集
rb3 := roaring.And(rb1, rb2)
// 输出结果
fmt.Println(rb3.ToArray())
// Output: [3]

4.3 差集运算

// 创建两个 RoaringBitmap
rb1 := roaring.NewBitmap()
rb2 := roaring.NewBitmap()
// 添加元素
rb1.Add(1)
rb1.Add(2)
rb1.Add(3)
rb2.Add(3)
rb2.Add(4)
rb2.Add(5)
// 计算差集
rb3 := roaring.AndNot(rb1, rb2)
// 输出结果
fmt.Println(rb3.ToArray())
// Output: [1 2]

4.4 异或运算

// 创建两个 RoaringBitmap
rb1 := roaring.NewBitmap()
rb2 := roaring.NewBitmap()
// 添加元素
rb1.Add(1)
rb1.Add(2)
rb1.Add(3)
rb2.Add(3)
rb2.Add(4)
rb2.Add(5)
// 计算异或
rb3 := roaring.Xor(rb1, rb2)
// 输出结果
fmt.Println(rb3.ToArray())
// Output: [1 2 4 5]

小结一下,RoaringBitmap 可以很方便地进行集合运算,这些运算都可以在高效地处理大规模数据集的同时,避免内存溢出和性能问题。同时,RoaringBitmap 还提供了丰富的 API 接口,支持更多高级的操作和应用场景。

5 总结

本文阐述了 RoaringBitmap的基础原理、数据结构和 Container 源码,也列举了 Go 语言常用的位运算。因为最近在业务场景里使用到了 RoaringBitmap,所以想和 xdm 介绍一下。在大数据的应用场景使用 RoaringBitmap 确实能够达到降本增效的作用。

大数据方面还有很多方向可以做,比如通过 RoaringBitmap 优化 Redis 中自带的 bitmap,通过 RoaringBitmap 也可以提高、优化 Flink 存储和计算去重状态的性能等等。

以上就是RoaringBitmap原理及在Go中的使用详解的详细内容,更多关于Go RoaringBitmap原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go压缩位图库roaring安装使用详解

    目录 简介 安装 使用 基本操作 迭代 并行操作 写入与读取 64 位版本 存储格式 概览 Cookie Header Descriptive Header Offset Header Container array bitmap/bitset run 手撸解析代码 总结 简介 集合是软件中的基本抽象.实现集合的方法有很多,例如 hash set.tree等.要实现一个整数集合,位图(bitmap,也称为 bitset 位集合,bitvector 位向量)是个不错的方法.使用 n 个位(bit)

  • go数据结构和算法BitMap原理及实现示例

    目录 1. BitMap介绍 如何判断数字在bit数组的位置 设置数据到bit数组 从bit数组中清除数据 数字是否在bit数组中 2. Go语言位运算 左移 右移 使用&^和位移运算来给某一位置0 3. BitMap的Go语言实现 定义 创建BitMap结构 将数据添加到BitMap 从BitMap中删除数据 判断BitMap中是否存在指定的数据 1. BitMap介绍 BitMap可以理解为通过一个bit数组来存储特定数据的一种数据结构.BitMap常用于对大量整形数据做去重和查询.在这类查

  • go reflect要不要传指针原理详解

    目录 正文 什么时候传递指针? 1. 通过传递指针修改变量的值 传值无法修改变量本身 传指针可以修改变量 2. 通过传递指针修改结构体的字段 3. 结构体:获取指针接收值方法 4. 变量本身包含指向数据的指针 通过值反射对象修改 chan.map 和 slice slice 反射对象扩容的影响 slice 容量够的话是不是就可以正常追加元素了? map 也不能通过值反射对象来修改其元素. chan 没有追加 结构体字段包含指针的情况 5. interface 类型处理 interface 底层类

  • go sync Once实现原理示例解析

    目录 正文 Once 的实现 使用示例 Once 的一些工作机制 Once 详解 hotpath atomic.LoadUint32 atomic.StoreUint32 Mutex 总结 正文 在很多情况下,我们可能需要控制某一段代码只执行一次,比如做某些初始化操作,如初始化数据库连接等. 对于这种场景,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次. 当然我们也是可以自己通过 Mutex 实现 sync.Once 的功能,但是相比来说繁琐了那么一点, 因为我们不仅

  • go slice 扩容实现原理源码解析

    目录 正文 扩容的示例 实际扩容倍数 growslice 实现 growslice 实现步骤 growslice 源码剖析 总结 正文 基于 Go 1.19. go 的切片我们都知道可以自动地进行扩容,具体来说就是在切片的容量容纳不下新的元素的时候, 底层会帮我们为切片的底层数组分配更大的内存空间,然后把旧的切片的底层数组指针指向新的内存中: 目前网上一些关于扩容倍数的文章都是基于相对旧版本的 Go 的,新版本中,现在切片扩容的时候并不是那种准确的小于多少容量的时候就 2 倍扩容, 大于多少容量

  • RoaringBitmap原理及在Go中的使用详解

    目录 引言 1 什么是 RoaringBitmap 2 数据结构 3 三种 Container 3.1 ArrayContainer 3.2 BitmapContainer 3.3 RunContainer 4 Go 使用 RoaringBitmap 4.1 并集运算 4.2 交集运算 4.3 差集运算 4.4 异或运算 5 总结 引言 今天我们聊聊 RoaringBitmap(咆哮位图).在海量数据背景下,我们通常需要快速对数据计算.中间存储的需求.一系列专门为大数据准备的数据结构应运而生,常

  • Java中的HashSet详解和使用示例_动力节点Java学院整理

    第1部分 HashSet介绍 HashSet 简介 HashSet 是一个没有重复元素的集合. 它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素. HashSet是非同步的.如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步.这通常是通过对自然封装该 set 的对象执行同步操作来完成的.如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来"包装" set.

  • Java 中的HashMap详解和使用示例_动力节点Java学院整理

    第1部分 HashMap介绍 HashMap简介 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口. HashMap 的实现不是同步的,这意味着它不是线程安全的.它的key.value都可以为null.此外,HashMap中的映射不是有序的. HashMap 的实例有两个参数影响其性能:"初始容量" 和 "加载因子&quo

  • Java中Volatile关键字详解及代码示例

    一.基本概念 先补充一下概念:Java内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的

  • bing Map 在vue项目中的使用详解

    写在最前面 拥有全球数据库国内好像就只有百度地图有,高德.搜狗.腾讯的都不行,但是由于百度地图的数据更新不及时,所以在做相关项目要用到国外数据的时候,最好还是推荐使用bingMap. bing Map 使用教程(基础) 参考文档:bing Map 官方教程 bing Map 初始化 引入bing map资源 <script type='text/javascript' src='http://www.bing.com/api/maps/mapcontrol?callback=GetMap&k

  • SSH原理及两种登录方法图文详解

    SSH(Secure Shell)是一套协议标准,可以用来实现两台机器之间的安全登录以及安全的数据传送,其保证数据安全的原理是非对称加密. 传统的对称加密使用的是一套秘钥,数据的加密以及解密用的都是这一套秘钥,可想而知所有的客户端以及服务端都需要保存这套秘钥,泄露的风险很高,而一旦秘钥便泄露便保证不了数据安全. 非对称加密解决的就是这个问题,它包含两套秘钥 - 公钥以及私钥,其中公钥用来加密,私钥用来解密,并且通过公钥计算不出私钥,因此私钥谨慎保存在服务端,而公钥可以随便传递,即使泄露也无风险.

  • scrapy爬虫:scrapy.FormRequest中formdata参数详解

    1. 背景 在网页爬取的时候,有时候会使用scrapy.FormRequest向目标网站提交数据(表单提交).参照scrapy官方文档的标准写法是: # header信息 unicornHeader = { 'Host': 'www.example.com', 'Referer': 'http://www.example.com/', } # 表单需要提交的数据 myFormData = {'name': 'John Doe', 'age': '27'} # 自定义信息,向下层响应(respon

  • Java ThreadLocal原理解析以及应用场景分析案例详解

    目录 ThreadLocal的定义 ThreadLocal的应用场景 ThreadLocal的demo TheadLocal的源码解析 ThreadLocal的set方法 ThreadLocal的get方法 ThreadLocalMap的结构 ThreadLocalMap的set方法 ThreadLocalMap的getEntry方法 ThreadLocal的内存泄露 如何避免内存泄露呢 应用实例 实际应用二 总结 ThreadLocal的定义 JDK对ThreadLocal的定义如下: The

  • Java中的ThreadLocal详解

    目录 一.ThreadLocal简介 二.ThreadLocal简单使用 三.ThreadLocal的实现原理 1.set方法源码 2.get方法源码 3.remove方法的实现 四.ThreadLocal不支持继承性 五.InheritableThreadLocal类 六.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1.基础概念 2.分析ThreadLocalMap内部实现 一.ThreadLocal简介 多线程访问同一个共享变量的时候容易出现并发问题,特别是

  • Java IO流之原理分类与节点流文件操作详解

    目录 IO流简介 IO流原理 流的分类 IO 流体系 节点流和处理流 节点流操作 IO流简介 I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输.如读/写文件,网络通讯等. Java程序中,对于数据的输入/输出操作以"流(stream)" 的方式进行. java.io包下提供了各种"流"类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据. IO流原理 输入input:读取外部数据(磁盘.光盘等存储设备的数据

随机推荐