Lucene 中的 VInt

三流發表於2022-05-29

零 版本

Lucene-Core 版本 8.8.2

一 簡介

Lucene 的 Index 設計基本依賴磁碟儲存,而倒排索引是依賴大量冗餘資料來完成分詞搜尋的技術,所以 Lucene 在設計的時候用了很多時間換空間的資料壓縮技術,以此保障能在最少的磁碟資源來儲存最多的資料。VInt 就是其中一個很有意思的結構設計。

二 技術原理

1 概要

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

        四 原始碼

        0 流程

        以下原始碼的呼叫流程:

  • lucene 確認一個 int 值
  • 呼叫 zigZagEncode(...) 將 int 編碼成 zint
  • 呼叫 writeVInt(...) 方法將 zint 編碼成 vint,並寫入到磁碟或者其它記憶體容器中
  • 呼叫 readVInt() 方法從磁碟或者其它記憶體容器中讀取一個 vint 值,並將其反編碼成 zint
  • 呼叫 zigZagDecode(...) 方法將 zint 反編碼成 int

    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 來說,會返回全 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 中:

    // decode 的操作和 zigZagEncode(...) 是完全相反的
    public static int zigZagDecode(int i) {
    return ((i >>> 1) ^ -(i & 1));
    }

相關文章