詳解varint編碼原理

什麼是Varint編碼

Varint是一種使用一個或多個字節序列化整數的方法,會把整數編碼爲變長字節。對於32位整型數據經過Varint編碼後需要1~5個字節,小的數字使用1個byte,大的數字使用5個bytes。64位整型數據編碼後佔用1~10個字節。在實際場景中小數字的使用率遠遠多於大數字,因此通過Varint編碼對於大部分場景都可以起到很好的壓縮效果。

編碼原理

除了最後一個字節外,varint編碼中的每個字節都設置了最高有效位(most significant bit - msb)–msb爲1則表明後面的字節還是屬於當前數據的,如果是0那麼這是當前數據的最後一個字節數據。每個字節的低7位用於以7位爲一組存儲數字的二進制補碼錶示,最低有效組在前,或者叫最低有效字節在前。這表明varint編碼後數據的字節是按照小端序排列的。

關於字節排列的方式引用一下維基百科上的詞條

字節的排列方式有兩個通用規則。例如,一個多位的整數,按照存儲地址從低到高排序的字節中,如果該整數的最低有效字節(類似於最低有效位)在最高有效字節的前面,則稱小端序;反之則稱大端序。在網絡應用中,字節序是一個必須被考慮的因素,因爲不同機器類型可能採用不同標準的字節序,所以均按照網絡標準轉化。

通俗一點說就是:大端序是按照數字的書寫順序排列的,而小端序是顛倒書寫順序進行排列的。

看下面的圖示會更好理解一些

圖片描述

圖中對數字123456進行varint編碼,123456用二進制表示爲1 11100010 01000000,每次從向高取7位再加上最高有效位變成1100 0000 11000100 00000111 所以經過varint編碼後123456佔用三個字節分別爲192 196 7

解碼的過程就是將字節依次取出,去掉最高有效位,因爲是小端排序所以先解碼的字節要放在低位,之後解碼出來的二進制位繼續放在之前已經解碼出來的二進制的高位最後轉換爲10進制數完成varint編碼的解碼過程。

編碼實現

由於protocol buffer中大量使用了varint編碼,我從github.com/golang/protobuf/proto庫中找到了對數據進行varint編解碼的Go語言實現方法,實現代碼中用位運算完成了上面說的varint編碼過程。

const maxVarintBytes = 10 // maximum length of a varint

// 返回Varint類型編碼後的字節流
func EncodeVarint(x uint64) []byte {
    var buf [maxVarintBytes]byte
    var n int
    // 下面的編碼規則需要詳細理解:
    // 1.每個字節的最高位是保留位, 如果是1說明後面的字節還是屬於當前數據的,如果是0,那麼這是當前數據的最後一個字節數據
    //  看下面代碼,因爲一個字節最高位是保留位,那麼這個字節中只有下面7bits可以保存數據
    //  所以,如果x>127,那麼說明這個數據還需大於一個字節保存,所以當前字節最高位是1,看下面的buf[n] = 0x80 | ...
    //  0x80說明將這個字節最高位置爲1, 後面的x&0x7F是取得x的低7位數據, 那麼0x80 | uint8(x&0x7F)整體的意思就是
    //  這個字節最高位是1表示這不是最後一個字節,後面7爲是正式數據! 注意操作下一個字節之前需要將x>>=7
    // 2.看如果x<=127那麼說明x現在使用7bits可以表示了,那麼最高位沒有必要是1,直接是0就ok!所以最後直接是buf[n] = uint8(x)
    //
    // 如果數據大於一個字節(127是一個字節最大數據), 那麼繼續, 即: 需要在最高位加上1
    for n = 0; x > 127; n++ {
        // x&0x7F表示取出下7bit數據, 0x80表示在最高位加上1
        buf[n] = 0x80 | uint8(x&0x7F)
        // 右移7位, 繼續後面的數據處理
        x >>= 7
    }
    // 最後一個字節數據
    buf[n] = uint8(x)
    n++
    return buf[0:n]
}
  • 0x7F的二進制表示是0111 1111 ,所以x & 0x7F 與操作時,得到x二進制表示的最後7個bit位(前面的bit爲通過與0做與操作都被捨棄了)
  • 0x80 的二進制表示是 1000 0000 ,所以 0x80 | uint8(x&0x7F)是在取出的x的後7個bit位前在最高位加上1(msb)

解碼實現

解碼就是編碼的逆過程,同樣是用位運算就能快速有效的完成解碼,結合下面的代碼註釋再在紙上推演一遍理解起來就不難了。

func DecodeVarint(buf []byte) (x uint64, n int) {
    for shift := uint(0); shift < 64; shift += 7 {
        if n >= len(buf) {
            return 0, 0
        }
        b := uint64(buf[n])
        n++
    // 下面這個分成三步走:
        // 1: b & 0x7F 獲取下7bits有效數據
        // 2: (b & 0x7F) << shift 由於是小端序, 所以每次處理一個Byte數據, 都需要向高位移動7bits
        // 3: 將數據x和當前的這個字節數據 | 在一起
        x |= (b & 0x7F) << shift
        if (b & 0x80) == 0 {
            return x, n
        }
    }

    // The number is too large to represent in a 64-bit value.
    return 0, 0
}

Playground

到這裏varint的編解碼過程就都搞懂了,理解了varint編碼原理後再看protocol buffer的編碼原理就會容易很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章