lucene
中
org.apache.lucene.util.packed
包主要提供了高效压缩long数组和解压的方法,以便压缩索引文件大小,以及快速读取索引文件内容。
核心类结构
重要概念
bitsPerValue
压缩一个long[]数组时,如果元素大小相差不大,找到最大的元素编码所需要的bit位个数(bitsPerValue),则所有元素都可以用这么多的bit位编码,从而达到压缩目的。
比如 long数组 [ 0, 1, 3, 5, 6, 7] 最大的元素7需要3个bit位才能编码,这个数组压缩后需要 6(元素个数) * 3(bitsPerValue) = 18个Bit即可编码。由于文件的写入单位是字节(8Bit), 所以需要4个字节。
byteBlockCount, byteValueCount, iterations
byteBlockCount
和byteValueCount
两者不能孤立来看,必须结合起来分析。它表示当单个源数据(int或者long) 需要bitsPerValue
个bit来表示时,以byte来编码后, byteBlockCount
个字节正好能够表示byteValueCount
个源数据。
举个例子,单个源数据需要7个Bit来表示,则byteBlockCount=7, byteValueCount=8,表示
7个byte共计56个Bit可以表达8个源数据。所以说 byteBlockCount,byteValueCount 密切相关,不可分离。
那iterations
又是什么呢?我这里给它叫做迭代批次, 我们知道编码后的值我们最终要flush
进入文件的,那么何时写入文件就是一个需要探究的话题了。lucene中认为iterations
迭代批次占用大约1024B内存或者刚好可以编码valueCount
个源数据就可以flush
一次了。
比如现在有783(valueCount=783
)个long型源数据要编码为字节数组, 最大元素需要7个Bit表示(bitsPerValue=7
) , 则 byteBlockCount=7, byteValueCount=8
, 即编码后每7个字节可表达8个源数据,则一个迭代批次映射到的就是编码后的7个字节,可以编码前的8个源数据,表达这个批次所需要的内存为 byteBlockCount + 8 * byteValueCount(一个long需要8字节)。
那个需要多少了iteration呢?1024/ (7 + 8*8) = 14,则需要14个iterations进行flush一次。这是因为源数据个数很多, 783个, flush一次后还得继续压缩。如果源数据个数不多呢?那么需要 valueCount/byteValueCount向上取整个iteration即可
// BulkOperation.java
public final int computeIterations(int valueCount, int ramBudget) {
// byteValuesCount表示可表达的源数据个数,这里假设源数据是long型,每个long->8byte
// byteBlockCount表示源数据编码后需要多少个byte block来表示
// 8 * byteValueCount() 表示源数据本身需要多少个byte来表示
final int iterations = ramBudget / (byteBlockCount() + 8 * byteValueCount());
if (iterations == 0) {
// at least 1
return 1;
} else if ((iterations - 1) * byteValueCount() >= valueCount) {
// don't allocate for more than the size of the reader
// 当valueCount源数据个数太少,根本用不了1024byte时, 计算所需要的iterations迭代次数,一个迭代批里就能表达byteValueCount个源数据
return (int) Math.ceil((double) valueCount / byteValueCount());
} else {
return iterations;
}
}
编码
在lucene
的packed
包中对long数组编码有两种方式,一种是紧凑型COMPACT
,一种是填充对齐型PACKED_SINGLE_BLOCK
。见源码
COMPACT
举例说明,将long数组编码到byte数组中。
输入long数组按照每个元素需要bitsPerValue个Bit位,依次填充到目标byte[]数组中,比如
输入数组为 [0, 0, 3, 5, 7], 则填充到目标byte[]数组后,变为如下图所示
核心编码如下,源码
// BulkOperationPacked.java
public void encode(long[] values, int valuesOffset, byte[] blocks,
int blocksOffset, int iterations) {
int nextBlock = 0;
int bitsLeft = 8;
for (int i = 0; i < byteValueCount * iterations; ++i) {
final long v = values[valuesOffset++];
assert PackedInts.unsignedBitsRequired(v) <= bitsPerValue;
if (bitsPerValue < bitsLeft) {
// just buffer
nextBlock |= v << (bitsLeft - bitsPerValue);
bitsLeft -= bitsPerValue;
} else {
// flush as many blocks as possible
int bits = bitsPerValue - bitsLeft;
blocks[blocksOffset++] = (byte) (nextBlock | (v >>> bits));
// 当bitsPerValue较大时,比如33,一个源数据就需要多个byte block
while (bits >= 8) {
bits -= 8;
blocks[blocksOffset++] = (byte) (v >>> bits);
}
// then buffer
bitsLeft = 8 - bits;
nextBlock = (int) ((v & ((1L << bits) - 1)) << bitsLeft);
}
}
// 因为finish时源数据个数没达到申请时的源数据个数时,会补0
assert bitsLeft == 8;
}
PACKED_SINGLE_BLOCK
编码long数组源数据到byte数组中,假设bitsPerValue=21, 那么3个源数据占用63个Bit,也就是3个源数据用一个long(8个byte)来编码差不多正好,浪费了一个Bit; 下一次3个源数据用一个新的long(8个byte)来编码; 总之就是每三个源数据需要8个byte来编码,这8个byte的末尾Bit没有用尽浪费就浪费掉了,但是由于对齐了,所以解码时的速度就更快。
// BulkOperationPackedSingleBlock.java
public void encode(long[] values, int valuesOffset, byte[] blocks, int blocksOffset, int iterations) {
for (int i = 0; i < iterations; ++i) {
// block的末尾几个bit可能没有用到
final long block = encode(values, valuesOffset);
valuesOffset += valueCount;
// 将block写到blocks里去,占用8个byte
blocksOffset = writeLong(block, blocks, blocksOffset);
}
}
private long encode(long[] values, int valuesOffset) {
long block = values[valuesOffset++];
// 这里需要注意到在一个block中,靠前面的源数据编码到block的低位上
// 这是与COMPACT编码不同的
for (int j = 1; j < valueCount; ++j) {
block |= values[valuesOffset++] << (j * bitsPerValue);
}
return block;
}
protected int writeLong(long block, byte[] blocks, int blocksOffset) {
for (int j = 1; j <= 8; ++j) {
blocks[blocksOffset++] = (byte) (block >>> (64 - (j << 3)));
}
return blocksOffset;
}
解码
解码核心类图关系如下
1⃣️编码时会将bitsPerValue, valueCount, formatId
等信息写到输出文件的头部,解码时最先把它们取出来,决定使用哪一个Reader
实现类来解码。
// PackedInts.java
public static Reader getReaderNoHeader(DataInput in, Format format, int version,
int valueCount, int bitsPerValue) throws IOException {
checkVersion(version);
switch (format) {
case PACKED_SINGLE_BLOCK:
return Packed64SingleBlock.create(in, valueCount, bitsPerValue);
case PACKED:
switch (bitsPerValue) {
case 8:
return new Direct8(version, in, valueCount);
case 16:
return new Direct16(version, in, valueCount);
case 32:
return new Direct32(version, in, valueCount);
case 64:
return new Direct64(version, in, valueCount);
case 24:
if (valueCount <= Packed8ThreeBlocks.MAX_SIZE) {
return new Packed8ThreeBlocks(version, in, valueCount);
}
break;
case 48:
if (valueCount <= Packed16ThreeBlocks.MAX_SIZE) {
return new Packed16ThreeBlocks(version, in, valueCount);
}
break;
}
return new Packed64(version, in, valueCount, bitsPerValue);
default:
throw new AssertionError("Unknown Writer format: " + format);
}
}
2⃣️假设我们这里以Packed64SingleBlock.Packed64SingleBlock1
进行解码,bitsPerValue
为1,每个源数据1个Bit就能表达,才会使用这个Reader解码。
// Packed64SingleBlock1.java
// index从0开始计算,对应的是编码时PackedInts.Writer依次add()的值
public long get(int index) {
final int o = index >>> 6; // 相当于除以64, index0-index63
// 都要从block[0]即第一个long中解析出来
final int b = index & 63; // 相当于除以64,取余数,余数为b
final int shift = b << 0; // 换个变量名称而已
// block long型
return (blocks[o] >>> shift) & 1L; // 就是取余数b对应的long中的那一位
}
可以看到,靠前的源数据是从block的低位取的,这与编码PACKED_SINGLE_BLOCK
时描述的是一致的。
3⃣️假设以Packed64
进行解码,bitsPerValue=3
, 看下构造函数
代码和get(int index)
代码
public Packed64(int packedIntsVersion, DataInput in, int valueCount, int bitsPerValue)
throws IOException {
super(valueCount, bitsPerValue);
final PackedInts.Format format = PackedInts.Format.PACKED;
// 计算valueCount个源数据编码后需要多少个byte来装载
final long byteCount = format.byteCount(packedIntsVersion,
valueCount, bitsPerValue); // to know how much to read
// 计算valueCount个源数据编码后需要多少个long来装载
final int longCount = format.longCount(PackedInts.VERSION_CURRENT, valueCount, bitsPerValue);
// to size the array
blocks = new long[longCount];
// 把编码后的byte数据装载到long数组中
// read as many longs as we can
for (int i = 0; i < byteCount / 8; ++i) {
blocks[i] = in.readLong();
}
final int remaining = (int) (byteCount % 8);
// 处理最后一个long,因为最后一个long没占满
if (remaining != 0) {
// read the last bytes
long lastLong = 0;
for (int i = 0; i < remaining; ++i) {
lastLong |= (in.readByte() & 0xFFL) << (56 - i * 8);
}
blocks[blocks.length - 1] = lastLong;
}
// maskRight掩码为0b111=7
maskRight = ~0L << (BLOCK_SIZE-bitsPerValue) >>> (BLOCK_SIZE-bitsPerValue);
// bpvMinusBlockSize = 3-64=-61
bpvMinusBlockSize = bitsPerValue - BLOCK_SIZE;
}
// 该方法用于取出当初依次add进去的源数据
public long get(final int index) {
// The abstract index in a bit stream
// 在源数据中Bit位的位置指针,比如index=1时, majorBitPos=3
final long majorBitPos = (long)index * bitsPerValue;
// The index in the backing long-array, /64
// 在编码后数据中,在long数组的位置, index=1时,elementPos = 0,
// 表示在编码后long数组的第一个long中
final int elementPos = (int)(majorBitPos >>> BLOCK_BITS);
// The number of value-bits in the second long
// index=1时, endBits = -58
final long endBits = (majorBitPos & MOD_MASK) + bpvMinusBlockSize;
if (endBits <= 0) {
// Single block , maskRight =7
// index=1时,blocks[0]右移58次,再&7,就等于取block[0]的58-61位数据
// 认为其就是源数据
return (blocks[elementPos] >>> -endBits) & maskRight;
}
// Two blocks
// index = 21时,我们知道blocks[0]最后一个Bit位(long最低位)被占用
// block[1]最开始2个Bit位(long最高2位)被占用, 要将这3个bit位依次连接起来
// 形成3个Bit来表达源数据
return ((blocks[elementPos] << endBits)
| (blocks[elementPos+1] >>> (BLOCK_SIZE - endBits)))
& maskRight;
}
其他的解码类读者自己看吧!!
总结
最好的学习方式是 看源码,看注释,多调试,多训练,你可以从lucene
的packed
包的源码测试用例 着手,去探索研究。