LevelDB 原始碼解析之 Varint 編碼

debugzhang發表於2021-03-31

GitHub: https://github.com/storagezhang

Emai: debugzhang@163.com

華為雲社群: https://bbs.huaweicloud.com/blogs/253047

LevelDB: https://github.com/google/leveldb

Varint 編碼

LevelDB 內部採用變長編碼,對資料進行壓縮,減少儲存空間,再採用 CRC 校驗資料。

整型資料是以 32(64) 位來表示的,以 32 位為例,儲存需要 4 個位元組。

如果一個整數的大小在 256 以內,那麼只需要一個位元組就可以儲存這個整數,可以節省 3 個位元組。

Varint 就是根據這種思想來序列化整數的,它是一種使用一個或多個位元組序列化整數的方法,會把整型資料編碼為變長位元組。

Varint 中的每個位元組都設定為最高有效位:

  • 如果該位為 0,表示結束,當前位元組的剩餘 7 位就是該資料的表示。

    • 表示整數 1,需要一個位元組:0000 0001
  • 如果該位為 1,表示後續的位元組也是該整型資料的一部分;

    • 表示整數 300,需要兩個位元組:1010 1100 0000 0010

這也表示 Varint 編碼後是按小端排序的。

位元組順序,又稱端序或尾序(英語:Endianness),在電腦科學領域中,指電腦記憶體中或在數字通訊鏈路中,組成多位元組的字的位元組的排列順序。

位元組的排列方式有兩個通用規則。例如,將一個多位數的低位放在較小的地址處,高位放在較大的地址處,則稱小端序;反之則稱大端序。在網路應用中,位元組序是一個必須被考慮的因素,因為不同機器型別可能採用不同標準的位元組序,所以均按照網路標準轉化。

因此,32 位整型資料經過 Varint 編碼後佔用 1~5 個位元組(5 * 8 - 5 > 32),64 位整型資料編碼後佔用 1~10 個位元組(10 * 8 - 10 > 64)。

在實際場景中,由於小數字的使用率遠遠高於大數字,所以在大部分場景中,通過 Varint 編碼的資料都可以起到很好的壓縮效果。

編碼實現

EncodeVarint64 將 uint64_t 編碼為 Varint 型別的位元組流:

char* EncodeVarint64(char* dst, uint64_t v) {
  static const int B = 128;
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);

  while (v >= B) {
    // B=128=0x80, v|B 表示在最高位上加 1
    // *ptr 是 uint8_t 型別的,即每次取下 7 位資料
    *(ptr++) = v | B;

    // 右移 7 位, 繼續處理後面的資料
    v >>= 7;
  }

  // 處理最後一個位元組的小於 128 的資料
  *(ptr++) = static_cast<uint8_t>(v);
  return reinterpret_cast<char*>(ptr);
}

EncodeVarint32uint32_t 編碼為 Varint 型別的位元組流,其實現與 EncodeVarint64 類似,但是可能因為最多 5 個位元組,所以是硬編碼的:

char* EncodeVarint32(char* dst, uint32_t v) {
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  static const int B = 128;

  if (v < (1 << 7)) {
    // v < 0x80,可以用 7 位表示,佔一個位元組
    *(ptr++) = v;
  } else if (v < (1 << 14)) {
    // 0x80 <= v < 0x4000,可以用 14 位表示,佔兩個位元組
    *(ptr++) = v | B;
    *(ptr++) = v >> 7;
  } else if (v < (1 << 21)) {
    // 0x4000 <= v < 0x200000,可以用 21 位表示,佔三個位元組
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = v >> 14;
  } else if (v < (1 << 28)) {
    // 0x200000 <= v < 0x10000000,可以用 28 位表示,佔四個位元組
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = v >> 21;
  } else {
    // 0x10000000 <= v < 0x100000000,可以用 35 位表示,佔五個位元組
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = (v >> 21) | B;
    *(ptr++) = v >> 28;
  }
  return reinterpret_cast<char*>(ptr);
}

解碼實現

解碼就是編碼的逆過程,同樣是利用位運算進行。

GetVarint64Ptr 將輸入的 Varint 型別位元組流轉換成 uint64_t 整型資料:

const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) {
  uint64_t result = 0;
  for (uint32_t shift = 0; shift <= 63 && p < limit; shift += 7) {
    uint64_t byte = *(reinterpret_cast<const uint8_t*>(p));
    p++;

    if (byte & 128) {
      // byte & 0x80 判斷最高有效位為 1

      // byte & 0x7f:獲取 7 位有效資料
      // (b & 0x7F) << shift:Varint 編碼是小端排序,每處理一個資料,都需要向高位移動 7 位
      // result | ((byte & 127) << shift):連線高位資料和低位資料
      result |= ((byte & 127) << shift);
    } else {
      // byte & 0x80 判斷最高有效位為 0,最後 7 位資料
      result |= (byte << shift);
      *value = result;
      return reinterpret_cast<const char*>(p);
    }
  }
  return nullptr;
}

GetVarint32Ptr 與GetVarint64Ptr 演算法相同,唯一的區別在於對小於 128 的資料進行特判,如果小於則直接返回結果,這樣設計的原因是大部分數字都比 128 小,可以通過行內函數提高計算效率。

inline const char* GetVarint32Ptr(const char* p, const char* limit,
                                  uint32_t* value) {
  if (p < limit) {
    uint32_t result = *(reinterpret_cast<const uint8_t*>(p));
    if ((result & 128) == 0) {
      *value = result;
      return p + 1;
    }
  }
  return GetVarint32PtrFallback(p, limit, value);
}

const char* GetVarint32PtrFallback(const char* p, const char* limit,
                                   uint32_t* value) {
  uint32_t result = 0;
  for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) {
    uint32_t byte = *(reinterpret_cast<const uint8_t*>(p));
    p++;
    if (byte & 128) {
      result |= ((byte & 127) << shift);
    } else {
      result |= (byte << shift);
      *value = result;
      return reinterpret_cast<const char*>(p);
    }
  }
  return nullptr;
}

相關文章