Luence-ByteBlockPool动态数组

1,007 阅读5分钟

背景

在luence的倒排表实现逻辑中,需要将term信息构造成一个倒排索引,在构造的过程中需要根据不断的根据当前处理的term和document的信息不断的申请数组。由于不知道具体需要具体需要多少byte[],存在扩容的需求,为了避免不断的在不同的byte内部进行拷贝,luence实现了一套自己的动态扩缩数组,在很多需要不断构造数组的场景下可以接见你luence的实现方式。

数据结构

public byte[][] buffers; //实际的内存结构,由一个byte的二维数组构造而成
private int bufferUpto = -1;//当前生效的buffer的index,出事状态下buffer为空
public int byteUpto;//当前可用的buffer,中可用的size大小
public byte[] buffer;// 当前可用的buffer带下
public int byteOffset;// 整个buffer的byte的offset粒度

下图简单描述了一个ByteBlockPoold实际内存结构

首先,我们有一个buffers的二维byte数组,这个二维数组是用于维护整个byte的pool。

数组的每个item,可以被称为一个BLOCK,block的大小是固定的,每当当前pool的大小不再满足需要钱时我们需要申请新的BLOCK

整个block是可以分配给多个数据组的,比如有2个数据组分别用图中的蓝色和绿色的颜色标识。

每次数据源写入的时候会首次会分配一个数组,这个byte数组被称为slice。当这个slice用完了之后就会分配一个新的slice,

slice本身是分级别的,不同级别的slice分配的大小是不同的,每次当前的数据组新申请slice的时候都会将当前的级别+1,一共有9个级别,当升级到10级时,就维持到当前级别,不会进一步扩大。

public static final int[] NEXT_LEVEL_ARRAY = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};
public static final int[] LEVEL_SIZE_ARRAY = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};

比如首次分配的时候slice的级别为1,对应的slice大小为5,当这个5个byte用完了就会分配一个新的level=2的slice,对应的size为14,对应的数据的offset就是5-18。但是当级别增长到9的时候,NEXT_LEVEL_ARRAY的[8]还是9,所以这时候会维持到level=9的状态 。

需要说明的是不同数据组是密集的写在ByteBlockPool上的,同一个数据组的slice会被分散的分配到整个pool上,实际上每个slice的尾部4个字节会写入同一个数据组的下一个slice的offset。

代码逻辑

ByteBlockPool其实是一个非常内部的类,甚至数据成员都是public实现,上游调用均来自于TermsHashPerField。基本上只涉及到2类操作,一类操作是用于扩容相关的逻辑,一类操作是与BytesRef进行交互。

扩容

扩容只有2类扩容,一类是针对block的扩容,一类是针对slice的扩容。

  • block扩容

针对block的扩容比较简单:基本的逻辑就是当当前整个pool都没有足够的空余byte分配给新的slice的时候就需要扩容block了

public void nextBuffer() {
  if (1 + bufferUpto == buffers.length) {
    // 这里如果二维数组的大小不够了会扩容二维数组的大小,但是只需要copy一下二维数组就好。整体的成本低很多
    byte[][] newBuffers =
        new byte[ArrayUtil.oversize(buffers.length + 1, NUM_BYTES_OBJECT_REF)][];
    System.arraycopy(buffers, 0, newBuffers, 0, buffers.length);
    buffers = newBuffers;
  }
  buffer = buffers[1 + bufferUpto] = allocator.getByteBlock();
  bufferUpto++;
  // 重置了byteUpto,更新了整体的byteOffset
  byteUpto = 0;
  byteOffset += BYTE_BLOCK_SIZE;
}
  • slice扩容

针对slice的扩容就要复杂很多了。具体来说是2种场景,

一种是直接已知需要多少size,并直接new一个slize,这种场景比较简单,不再讨论

public int newSlice(final int size) {
  // 实际上这里的size永远都是level1的size,也就是5
  if (byteUpto > BYTE_BLOCK_SIZE - size) nextBuffer();
  final int upto = byteUpto;
  byteUpto += size;
  // 在slice的结尾会写如一个非0的参数这个参数实际的值是 16|level,这个参数有2个作用
  // 非0 标识当前slice结束
  // 用于标识当前slice的level
  buffer[byteUpto - 1] = 16;
  return upto;
}

另一种是根据当前slice来申请一个新slice,这里的实现就比较有意思了,首先上一下代码:

//首先要看懂的是输入参数是什么含义。
//这里的slice数组,其实是当前slice所属的byte数组,
//而upto指的是这个数组的一个偏移量,这个偏移量所代表的含义仅仅是当前slice的终止位置。
public int allocSlice(final byte[] slice, final int upto) {

  // 从最后一个位置提取level
  final int level = slice[upto] & 15;
  final int newLevel = NEXT_LEVEL_ARRAY[level];
  final int newSize = LEVEL_SIZE_ARRAY[newLevel];

  if (byteUpto > BYTE_BLOCK_SIZE - newSize) {
    nextBuffer();
  }

  final int newUpto = byteUpto;
  final int offset = newUpto + byteOffset;
  byteUpto += newSize;

  int past3Bytes = ((int) BitUtil.VH_LE_INT.get(slice, upto - 3)) & 0xFFFFFF;
  assert buffer[newUpto + 3] == 0;
  BitUtil.VH_LE_INT.set(buffer, newUpto, past3Bytes);
  BitUtil.VH_LE_INT.set(slice, upto - 3, offset);
  buffer[byteUpto - 1] = (byte) (16 | newLevel);

  return newUpto + 3;
}

从19行23行开始变得复杂了起来,其实希望做的是这样一件事情,从原先的slice的尾部拷贝出来3个byte(实际操作是用来BitUtil按小端序读出来4个字节,然后和0xFFFFFF进行位与)

将这三个值写入new-slice的前3位,同时将old-slice的最后4个byte(3byte已经被copy出来,1byte原来用于存储(16|old-Level))写为新的offset的值。