lucene工具包packed概述


luceneorg.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
byteBlockCountbyteValueCount两者不能孤立来看,必须结合起来分析。它表示当单个源数据(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;
    }
  }

编码

lucenepacked包中对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;
  }

其他的解码类读者自己看吧!!


总结

最好的学习方式是 看源码,看注释,多调试,多训练,你可以从lucenepacked包的源码测试用例 着手,去探索研究。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值