背景
在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的值。