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 項目,都在一些場景使用了這套連招,快快使用吧~.
完。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十