lucene 代码量仍是比较多的,在没有看的很明白的状况下,先写一写新学到的工具类的一些操做吧~也是收获不少。java
在 lucene 写入索引文件时,为了节省空间,常常会对数据进行一些压缩,这篇文章介绍一种对 int, long 类型有用的压缩方式。即变长存储。apache
它在 lucene 中的应用十分普遍,有事没事就用一下,所以为了熟练的理解代码,咱们仍是来一探究竟吧~微信
在 lucene8.7.0 版本的代码中,它没有单独定义成类,多是由于是一个小的功能点吧~markdown
对变长数据的写入实如今org.apache.lucene.store.DataOutput#writeVInt
中,对变长数据的读取实如今org.apache.lucene.store.DataInput#readVInt
.app
什么叫作变长存储?咱们以writeVInt
为例,看看注释:ide
Writes an int in a variable-length format. Writes between one and five bytes. Smaller values take fewer bytes. Negative numbers are supported, but should be avoided.函数
VByte is a variable-length format for positive integers is defined where the high-order bit of each byte indicates whether more bytes remain to be read. The low-order seven bits are appended as increasingly more significant bits in the resulting integer value. Thus values from zero to 127 may be stored in a single byte, values from 128 to 16,383 may be stored in two bytes, and so on.工具
简单翻译一下:oop
以可变长度格式写入一个整数。写入 1-5 个字节。越小的值占用的字节越少。支持负数可是尽可能别用。学习
VByte 是正整数的变长格式,每一个 byte 的高位用来标识是否还有更多的字节须要读取。低位的 7 个 bit 位表明实际的数据。将逐渐读取到的低位附加做为愈来愈高的高位,就能够拿到原来的整数。
0127 只须要一个字节,12816383 须要两个字节,以此类推。
从这里看到,变长整数存储的压缩率,是和数字大小有关系的,数字越小,压缩率越高,若是全是最大的 int, 反而须要更多的字节来存储。
咱们实现一个简单的工具类,能实现上述的变长存储 (lucene 代码 copy 出来), 以外提供一些辅助咱们看源码的方法。
public class VariableInt {
/** * transfer int to byte[] use variable format */
public static byte[] writeVInt(int i) {
int bytesRequired = bytesRequired(i);
byte[] res = new byte[bytesRequired];
int idx =0;
while ((i & ~0x7F) != 0) {
res[idx++] = ((byte) ((i & 0x7F) | 0x80));
i >>>= 7;
}
res[idx] = (byte) i;
return res;
}
/** * transfer byte[] to int use variable format */
public static int readVInt(byte [] vs) throws IOException {
int idx = 0;
byte b = vs[idx++];
// 大于 0, 说明第一位为 0, 说明后续没有数据须要读取
if (b >= 0) return b;
int i = b & 0x7F;
b = vs[idx++];
i |= (b & 0x7F) << 7;
if (b >= 0) return i;
b = vs[idx++];
i |= (b & 0x7F) << 14;
if (b >= 0) return i;
b = vs[idx++];
i |= (b & 0x7F) << 21;
if (b >= 0) return i;
b = vs[idx];
// Warning: the next ands use 0x0F / 0xF0 - beware copy/paste errors:
i |= (b & 0x0F) << 28;
if ((b & 0xF0) == 0) return i;
throw new IOException("Invalid vInt detected (too many bits)");
}
/** * compute int need bytes. */
public static int bytesRequired(int i) {
if (i < 0) throw new RuntimeException("I Don't Like Negative.");
if ((i >>> 7) == 0) return 1;
if ((i >>> 14) == 0) return 2;
if ((i >>> 21) == 0) return 3;
if ((i >>> 28) == 0) return 4;
return 5;
}
}
复制代码
除了读取写入意外,提供了一个计算 int 数字须要几个 byte 来存储的方法。在咱们 debug 源码时,能够帮助咱们分析写入的索引文件。
VariableLong 的代码就不贴了。和 Variable 基本相同,只是变长的长度从 1-5 变成了 1-9 而已。
在 Lucene 实现的 DataOutPut 中,咱们能够看到writeZint(int i)
方法,通过了解,它使用 zigzag 编码+变长存储来存储一个整数。
什么是 zigzag 编码?
首先咱们回顾一下计算机编码:
为了方便及其余问题,计算机使用补码来存储整数。
那么咱们的变长整数就有一个问题。他对于负数很不友好。
11111111111111111111111111111111
, 也就是说所有是 1. 你这时候用变长编码来存储,须要 5 个字节,压缩的目的达不到了。反而多占了空间。那么基于一个共识:小整数用的多,所以须要变长编码. 小的负整数也很多,变长编码会压缩率不高甚至反向压缩.
所以诞生了 zigzag 编码,它能够有效的处理负数。它的底层逻辑是:按绝对值升序排列,将整数 hash 成递增的 32 位 bit 流,其 hash 函数为 h(n) = (n << 1) ^ (n >> 31),
hash 函数的做用如图所示:
设想一下这个 hash 函数作了什么?
对于小的负整数而言:
那么-1 的表示变成了00000000000000000000000000000001
, 比较小,适合使用变长编码了。 1 的表示变成了00000000000000000000000000000010
, 虽然增大了一点,可是仍然很小,也适合使用变长编码了。
总结一下:
zigzag 编码解决了使用变长编码时小的负整数压缩率过低的问题,它基于一个共识,就是咱们使用的小整数(包括正整数和负整数) 是比较多的。所以将负整数映射到正整数这边来操做。
对应表是:
整数 | zigzag |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2 | 4 |
-3 | 5 |
3 | 6 |
这个 zigzag 的实现比较简单,在上面已经实现了变长编码的基础上。只须要实现一个简单的 hash 函数就行了。
/** * transfer int to byte[] use zig-zag-variable format */
public static byte[] writeZInt(int i) {
// zigzag 编码
i = (i >> 31) ^ (i << 1);
return writeVInt(i);
}
/** * transfer byte[] to int use zig-zag-variable format */
public static int readZInt(byte[] vs) throws IOException {
int i = readVInt(vs);
return ((i >>> 1) ^ -(i & 1));
}
复制代码
完美。
本文简单介绍了。
所以,当你确认你的待压缩数字,都是比较小的正负整数,就使用 zigzag+变长编码来进行压缩吧,压缩率 25~50%仍是能够作到的。
不少须要序列化的开源程序,都是用 zigzag+变长编码来进行整数的压缩,好比 google 的 protobuf, apache 的 avro 项目,apache 的 lucene 项目,都在一些场景使用了这套连招,快快使用吧~.
完。
以上皆为我的所思所得,若有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文连接。
更多学习笔记见我的博客或关注微信公众号 < 呼延十 >------>呼延十