bitmap plus -> RoaringBitmap原理、源码分析~

前言

很简单,最近看到了个以前没了解过过的数据结构RoaringBitmap,所以想了解了解,顺便整理成一篇文章分享出来~


简介

RoaringBitmap去掉Roaring后就剩下bitmap,那么很显然,RoaringBitmapbitmap的升级版,或者说在某些方面更优~

例如: ConcurrentHashMap 和 HashMapMybatisPlus 和 Mybatis等~ ,当然JavaScript 和 Java除外

哈哈,回到文章,说到bitmap,我们都知道其底层是bit数组 ,因为在存储方面可以极大的节省空间,具体怎么节省呢~ 可以接着看看下面👇🏻

Javaint类型占4个字节,1字节 = 8bit

那么如果此时我们要存储10亿int类型数字的话

  • 正常我们需要1000000000 * 4 / 1024 / 1024 ≈ 3814M
  • 那么用bitmap来存储的话,则需要1000000000 / 8 / 1024 / 1024 ≈ 119M

这么一对比,bitmap的优势就显而易见了,但有优点也会存在一定的缺点即不适合稀疏数据的存储比如我要存储[1, 100, 1000, 10000000]的bit数据,那么bitmap申请的空间至少为10000000 / 8 / 1024 / 1024 ≈ 1.19M ,但实际我只存储了4个数据,这样就造成了很大一部分的空间浪费~

故,RoaringBitmap出世了!!! ,其解决了在存储大规模或稀疏数据时的一些缺点,并提供更好的压缩率和高性能的操作


RoaringBitmap实现原理

RoaringBitmap32位的int类型数据分为高16位低16位高16位产生2^16个桶,那么在存储和查找数据时,根据高16位定位到对应的桶,再根据低16位来确定放置的桶中的位置~

比如我们要存储: 2、65537、131072三个数字

  • 数字2高16位为0低16位为2
  • 数字65537高16位为1低16位为1
  • 数字131072高16位为2,低16位为0`

上面的图乍一看像是HashMap数组 + 链表结构,但其实并不是;

RoaringBitmap中的"链表"主要以下几种实现

  • ArrayContainer
  • BitmapContainer
  • Runcontainer

ArrayContainer

听这名字就知道底层是采用的数组实现的ArrayContainer会将低16位直接保存到数组中,同时保持有序,方面二分查找,如果存储空间不够会现行扩容在进行存储;

这样的存储方式并没有进行压缩,因为不适合存储大量数据,最大存储数量默认为4096,超过这个阈值即会将ArrayContainer转为BitmapContainer

结合图示来看

  • 当存储个数小于4096BitmapContainer的内存占用比ArrayContainer

  • 当存储个数大于4096ArrayContainer的内存占用比BitmapContainer


BitmapContainer

底层采用bitmap实现,采用long[]存储数据,long8个字节即64bit,而我们存储的是低16位数据,最大为6553665536 / 64 = 1024,所以需要长度为1024long[]

所以无论存储1个还是1024个元素,占用空间都是8byte * 1024 = 8kb


Runcontainer

RunContainer采用行程长度压缩算法(Run Length Encoding) ,对于连续数据有比较好的压缩效果

原理是: 对于连续出现的数字,只记录初始数字和后续数量。如下👇🏻

  • 对于数列11,它会压缩为11,0
  • 对于数列11,12,13,14,15,它会压缩为11,4
  • 对于数列11,12,13,14,15,21,22,它会压缩为11,4,21,1

低版本 源码中采用short[] valueslength存就是压缩后的数据,高版本中改成了char[] valueslength

那么对于RunContainer来说,最差的情况就是存储0 ~ 65525的所有奇数或者所有偶数 ,这样一来就不会进行压缩 ,即最大占用空间为(65536 / 2) * 2byte / 1024 ≈ 64M


RoaringBitmap使用

先导入maven依赖

xml 复制代码
<dependency>
    <groupId>org.roaringbitmap</groupId>
    <artifactId>RoaringBitmap</artifactId>
    <version>0.9.44</version>
</dependency>

下面简单列了下常用的api,更多的话可以去网上搜搜啦~

java 复制代码
public class RoaringBitMapTest {

   public static void main(String[] args) {
      RoaringBitmap bitmap_1 = new RoaringBitmap();
      for (int i = 0; i < 10; i++) {
         // 添加元素
         bitmap_1.add(i);
      }

      // 打印元素
      System.out.println(Arrays.toString(bitmap_1.stream().toArray()));

      // 判断元素是否存在
      System.out.println(bitmap_1.contains(0));
      System.out.println(bitmap_1.contains(100));
      
      RoaringBitmap bitmap_2 = new RoaringBitmap();
      for (int i = 5; i < 20; i++) {
         // 添加元素
         bitmap_2.add(i);
      }

      // 合并bitmap_1、bitmap_2,取交集
      RoaringBitmap andBitmap = RoaringBitmap.and(bitmap_1, bitmap_2);
      System.out.println(Arrays.toString(andBitmap.stream().toArray()));

      // 合并bitmap_1、bitmap_2,取并集并去重
      RoaringBitmap orBitmap = RoaringBitmap.or(bitmap_1, bitmap_2);
      System.out.println(Arrays.toString(orBitmap.stream().toArray()));

      // 取bitmap_1有但bitmap_2没有的数据
      RoaringBitmap integers = RoaringBitmap.andNot(bitmap_1, bitmap_2);
      System.out.println(Arrays.toString(integers.stream().toArray()));
   }

}

RoaringBitmap源码

构造函数

java 复制代码
public class RoaringBitmap implements Cloneable, Serializable, Iterable<Integer>, Externalizable,
ImmutableBitmapDataProvider, BitmapDataProvider, AppendableStorage<Container> {

  RoaringArray highLowContainer = null;

  public RoaringBitmap() {
    // 初始化桶
    highLowContainer = new RoaringArray();
  }

}

public final class RoaringArray implements Cloneable, Externalizable, AppendableStorage<Container> {

  static final int INITIAL_CAPACITY = 4;

  char[] keys = null;

  Container[] values = null;

  int size = 0;

  protected RoaringArray() {
    // 默认容量为4
    this(INITIAL_CAPACITY);
  }

  RoaringArray(int initialCapacity) {
    this(new char[initialCapacity], new Container[initialCapacity], 0);
  }


  RoaringArray(char[] keys, Container[] values, int size) {
    // keys就是前面提到的高16位桶
    this.keys = keys;
    // values就是存储低16位的桶
    this.values = values;
    this.size = size;
  }

}

如上源码所示,RoaringBitmap构造函数中初始化了高16位桶低16位桶


add

java 复制代码
public void add(final int x) {
  // 拿到高16位
  final char hb = Util.highbits(x);
  // 根据高16位找到对应的桶index
  final int i = highLowContainer.getIndex(hb);
  if (i >= 0) {
    // 桶已存在,则根据低16位存储到桶内
    highLowContainer.setContainerAtIndex(i,
        highLowContainer.getContainerAtIndex(i).add(Util.lowbits(x)));
  } else {
    // 桶不存在,则创建并设置好桶,存放低16位数据
    final ArrayContainer newac = new ArrayContainer();
    highLowContainer.insertNewKeyValueAt(-i - 1, hb, newac.add(Util.lowbits(x)));
  }
}

步骤

  1. 拿到存储数据的高16位
  2. 根据高16位 找到对应的桶index
    1. 桶已存在,则根据低16位存储到桶内
    2. 桶不存在,则创建并设置好桶,存放低16位数据

contains

java 复制代码
public boolean contains(final int x) {
  // 获取高16位
  final char hb = Util.highbits(x);
  // 根据高16位拿到低16位对应的桶
  final Container c = highLowContainer.getContainer(hb);
  // 判断低16位是否存在
  return c != null && c.contains(Util.lowbits(x));
}

protected Container getContainer(char x) {
  // 根据高16位拿到桶对应的index
  int i = getContainerIndex(x);
  if (i < 0) {
    return null;
  }
  // 返回对应的桶
  return this.values[i];
}


// ArrayContainer
public boolean contains(final char x) {
  // 通过二分查找查询低16位index,如果大于等于0,说明存在
  return Util.unsignedBinarySearch(content, 0, cardinality, x) >= 0;
}

步骤

  1. 获取高16位
  2. 根据高16位获取低16位对应的桶
  3. ArrayContainer为例,contains内部通过二分搜索查询低16位index,如果大于等于0则说明存在

桶扩容

在上面的add方法中,如果桶不存在则会调用insertNewKeyValueAt进行创建并存储低16位数据

java 复制代码
void insertNewKeyValueAt(int i, char key, Container value) {
  // 检查是否需要扩容
  extendArray(1);
  
  // 存放桶并插入数据
  System.arraycopy(keys, i, keys, i + 1, size - i);
  keys[i] = key;
  System.arraycopy(values, i, values, i + 1, size - i);
  values[i] = value;
  size++;
}
java 复制代码
void extendArray(int k) {
  // 如果桶数量 + k 大于最大容量,则需要进行扩容
  if (this.size + k > this.keys.length) {
    int newCapacity;
    if (this.keys.length < 1024) {
      newCapacity = 2 * (this.size + k);
    } else {
      newCapacity = 5 * (this.size + k) / 4;
    }
    this.keys = Arrays.copyOf(this.keys, newCapacity);
    this.values = Arrays.copyOf(this.values, newCapacity);
  }
}

如果当前桶数量 + 要添加的桶数量 大于最大容量,则需要进行扩容

  • 当前桶容量小于1024 ,新容量为2 * (this.size + k)
  • 当前桶容量大于1024 ,新容量为5 * (this.size + k) / 4

扩展

上面说到的RoaringBitmap存储的都是int类型的数据,但是在实际业务中我们可能还需要存储long型的数据,这时候RoaringBitmap就不太适用了,我们需要改用Roaring64NavigableMap

api基本与RoaringBitmap一致,只是底层逻辑上会有变动,留到下次再进行探究~

java 复制代码
public class RoaringBitMapTest {

   public static void main(String[] args) {

      Roaring64NavigableMap roaringMap = new Roaring64NavigableMap();

      for (long i = 0; i < 10; i++) {
         roaringMap.addLong(i);
      }

      System.out.println(Arrays.toString(roaringMap.stream().toArray()));

      System.out.println(roaringMap.contains(0));
      System.out.println(roaringMap.contains(100));
   }

}

总结

本文从RoaringBitmap理论出发,横贯原理、源码,真可谓一条龙服务,求个赞不过分吧(狗头)


引用

相关推荐
期待のcode39 分钟前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐42 分钟前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
柳杉44 分钟前
建议收藏 | 2026年AI工具封神榜:从Sora到混元3D,生产力彻底爆发
前端·人工智能·后端
a程序小傲1 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红1 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥1 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v1 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地1 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209252 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei2 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot