lucene系列(二)int的變長存儲與zigzag編碼

前言

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 而已。

zigzag 編碼

在 Lucene 實現的 DataOutPut 中,咱們能夠看到writeZint(int i)方法,通過了解,它使用 zigzag 編碼+變長存儲來存儲一個整數。

什麼是 zigzag 編碼?

首先咱們回顧一下計算機編碼:

  • 原碼:最高位爲符號位,剩餘位表示絕對值;
  • 反碼:除符號位外,對原碼剩餘位依次取反;
  • 補碼:對於正數,補碼爲其自身;對於負數,除符號位外對原碼剩餘位依次取反而後+1。

爲了方便及其餘問題,計算機使用補碼來存儲整數。

那麼咱們的變長整數就有一個問題。他對於負數很不友好。

  • 1 這個 int 整數,自己存儲使用 4 個字節,經過上文的變長編碼,使用一個字節便可。
  • -1 這個 int 整數,他的補碼爲:11111111111111111111111111111111, 也就是說所有是 1. 你這時候用變長編碼來存儲,須要 5 個字節,壓縮的目的達不到了。反而多佔了空間。

那麼基於一個共識:小整數用的多,所以須要變長編碼. 小的負整數也很多,變長編碼會壓縮率不高甚至反向壓縮.

所以誕生了 zigzag 編碼,它能夠有效的處理負數。它的底層邏輯是:按絕對值升序排列,將整數 hash 成遞增的 32 位 bit 流,其 hash 函數爲 h(n) = (n << 1) ^ (n >> 31),

hash 函數的做用如圖所示:

2021-01-24-02-28-35

設想一下這個 hash 函數作了什麼?

對於小的負整數而言:

  1. 左移 1 位能夠消去符號位,低位補 0
  2. 有符號右移 31 位將符號位移動到最低位,負數高位補 1,正數高位補 0
  3. 按位異或 對於正數來講,最低位符號位爲 0,其餘位不變 對於負數,最低位符號位爲 1,其餘位按位取反

那麼-1 的表示變成了00000000000000000000000000000001, 比較小,適合使用變長編碼了。 1 的表示變成了00000000000000000000000000000010, 雖然增大了一點,可是仍然很小,也適合使用變長編碼了。

總結一下:

zigzag 編碼解決了使用變長編碼時小的負整數壓縮率過低的問題,它基於一個共識,就是咱們使用的小整數(包括正整數和負整數) 是比較多的。所以將負整數映射到正整數這邊來操做。

對應表是:

整數 zigzag
0 0
-1 1
1 2
-2 3
2 4
-3 5
3 6

zigzag 實現

這個 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));
    }
複製代碼

完美。

總結

本文簡單介紹了。

  1. 使用變長編碼來對整數進行壓縮,對於小正整數能取得不錯的壓縮率。
  2. 使用 zigzag 編碼對整數進行編碼,能夠解決掉變長編碼對於小負整數壓縮率低的難點。

所以,當你確認你的待壓縮數字,都是比較小的正負整數,就使用 zigzag+變長編碼來進行壓縮吧,壓縮率 25~50%仍是能夠作到的。

不少須要序列化的開源程序,都是用 zigzag+變長編碼來進行整數的壓縮,好比 google 的 protobuf, apache 的 avro 項目,apache 的 lucene 項目,都在一些場景使用了這套連招,快快使用吧~.


完。


以上皆爲我的所思所得,若有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文連接。

聯繫郵箱:huyanshi2580@gmail.com

更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十

相關文章
相關標籤/搜索