3

Lucene 中的 VInt 应用

 2 years ago
source link: https://segmentfault.com/a/1190000040288364
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Lucene- Core 版本 8.8.2

Lucene 的 Index 设计基本依赖磁盘存储,而倒排索引是依赖大量冗余数据来完成分词搜索的技术,所以 Lucene 在设计的时候用了很多时间换空间的数据压缩技术,以此保障能在最少的磁盘资源来储存最多的数据。VInt 就是其中一个很有意思的设计。

二 技术原理

Java 中一个普通的 int 占据 4 个 byte。

但是当 int 的值为 -128 ~ 127 的时候,其实只需要一个 byte 就可以放得下了,其它三个 byte 都是无意义的冗余(其它几个 byte 所能代表的区间以此类推),真实能够用满这四个 byte 的情况并不多。VInt 的意思是 variant int,也就是可变的 int。其本质是按需分配,减少这种冗余。

2 byte 指示位

一个正常的 byte 有八个数据有效位,而 VInt 中只有七个,最高位变成了后一个 byte 的指示位。

  • 最高位为 1,代表后一个 byte 依然是当前数据
  • 最高位为 0,代表后面没有数据了

3 VInt 的副作用

  • 对于正数来说,由于只有七个数据位,所以当 int 的值比较大的时候,可能会需要 5 个 byte 才能表述当前的数据(这个问题无法被解决,VInt 也觉得无需解决,因为情况在真实生产中并不多)
  • 对于负数来说,最高位为 1,无法被压缩(引入 zigzag 编码)

4 zigzag 编码

使用位移和异或操作将首位的符号位挪到数据的最后一位。

三 Demo

假如需要分别序列化 1 / 200 / -1 这三个 int 数,则 VInt 算法的具体步骤为(有效数据标黄):

1 二进制化

  • 1 的二进制数为 00000000 00000000 00000000 00000001
  • 200 的二进制数为 00000000 00000000 00000000 11001000
  • -1 的二进制数为 11111111 11111111 11111111 11111110

2 向前位移一位,后面补 0

  • 1 处理后的二进制数为 00000000 00000000 00000000 00000010
  • 200 处理后的二进制数为 00000000 00000000 00000001 10010000
  • -1 处理后的二进制数为 11111111 11111111 11111111 11111100

3 异或操作

异或操作的本质是不同为 0,相同是 1。

  • 对于正数,异或一个 11111111 11111111 11111111 11111111

    • 1 的处理表达式为 00000000 00000000 00000000 00000010 ^ 11111111 11111111 11111111 11111111 = 00000000 00000000 00000000 00000010;
    • 200 的处理表达式为 00000000 00000000 00000001 10010000 ^ 11111111 11111111 11111111 11111111 = 00000000 00000000 00000001 10010000
  • 对于负数,异或一个 00000000 00000000 00000000 00000000

    • -1 的处理表达式为 11111111 11111111 11111111 11111100 ^ 00000000 00000000 00000000 00000000 = 00000000 00000000 00000000 00000011

4 八位为一个单位处理数字

以八位为一个单位读取数据,当读取到八位之后,将第一位看作是标记位,如果还有其它数据的话,再读取八位。

  • 对于数字 1 来说

    • 序列化过程:

      • 先读取七位 0000010,之前都为 0,没有数据,则前面补 0,为 00000010
    • 读取过程:

      • 读取序列化数据 00000010,第一位是 0,代表只有一个 byte,后面没有其它数据,故数据为 00000010
      • 最后一位是 0,代表是正数,与 11111111 异或操作,得到 00000010
      • 将数据往后挪一位,前端补 0,最终为 00000001
  • 对于数字 200 来说

    • 序列化过程:

      • 先读取七位 0010000,之前不都为 0,则前面补 1,为 10010000
      • 再读取七位 0000011,之前都为 0,则前面补 0,为 00000011
      • 组合数据为 10010000 00000011
    • 读取过程:

      • 读取序列化数据 10010000,第一位是 1,代表不止一个 byte,后面还有其它数据,故数据为 0010000
      • 再读取 00000011,第一位是 0,代表没有其它数据来,数据为 0000011
      • 组合数据为 00000001 10010000
      • 最后一位是 0,代表是正数,与 11111111 11111111 异或操作,得到 00000001 10010000
      • 将数据往后挪一位,前端补 0,最终为 00000000 11001000
  • 对于数字 -1 来说

    • 序列化过程:

      • 先读取七位 0000011,之前都为 0,没有数据,则前面补 0,为 00000011
    • 读取过程:

      • 读取序列化数据 00000011,第一位是 0,代表只有一个 byte,后面没有其它数据,故数据为 00000011
      • 最后一位是 1,代表是负数,与 00000000 异或操作,得到 11111100
      • 将数据往后挪一位,前端补 1,最终为 11111110

1 writeZInt

writeZInt(...) 方法在 org.apache.lucene.store.DataOutput 中:

// 这个方法用于写入一个 zigzag 编码之后的 int 值
public final void writeZInt(int i) throws IOException {
  // BitUtil.zigZagEncode(i) 用于 zigzag 编码
  writeVInt(BitUtil.zigZagEncode(i));
}

// 用于写入一个 VInt
public final void writeVInt(int i) throws IOException {
  while ((i & ~0x7F) != 0) {
    // writeByte(...) 方法用于将 byte 持久化到文件中,暂时无需关注
    writeByte((byte)((i & 0x7F) | 0x80));
    i >>>= 7;
  }
  writeByte((byte)i);
}

2 zigZagEncode

zigZagEncode(...) 方法在 org.apache.lucene.util.BitUtil 中:

// i >> 31 对于正数来说,会返回全 0 的屏障
// i >> 31 对于负数来说,会返回全 1 的屏障
public static int zigZagEncode(int i) {
  return (i >> 31) ^ (i << 1);
}

3 readZInt

readZInt(...) 方法在 org.apache.lucene.store.DataOutput 中:

public int readZInt() throws IOException {
  return BitUtil.zigZagDecode(readVInt());
}

public int readVInt() throws IOException {
  // 此处从磁盘读取一个 byte
  byte b = readByte();
  // b >= 0,代表最高位是 0,后续没有值了,以下雷同
  if (b >= 0) return b;
  int i = b & 0x7F;
  // 继续读取一个 byte
  b = readByte();
  i |= (b & 0x7F) << 7;
  if (b >= 0) return i;
  // 继续读取一个 byte
  b = readByte();
  i |= (b & 0x7F) << 14;
  if (b >= 0) return i;
  // 继续读取一个 byte
  b = readByte();
  i |= (b & 0x7F) << 21;
  if (b >= 0) return i;
  // 继续读取一个 byte,在 VInt 的编码下,最高五个 byte
  b = readByte();
  i |= (b & 0x0F) << 28;
  if ((b & 0xF0) == 0) return i;
  throw new IOException("Invalid vInt detected (too many bits)");
}

4 zigZagDecode

zigZagDecode(...) 方法在 org.apache.lucene.util.BitUtil 中:

public static int zigZagDecode(int i) {
  return ((i >>> 1) ^ -(i & 1));
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK