淺析 Protobuf 整形編碼方式:Varint 與 Zigzag

Protocol Buffer (簡稱Protobuf) 是Google出品的性能優異、跨語言、跨平臺的序列化庫。編程

爲了更好的硬件效率,計算機中的數字一般使用定長整形(fixed length intergers)表示。然而在遍地微服務的今天,須要一種更靈活的方式傳輸數字以節省帶寬。Varint (Variable length integers)即是一種用於編碼整形數字的方法,經過它能夠靈活地調整整形數值所須要的空間大小。bash

Varint 編碼

Protobuf 中的 Varint 根據整型大小進行不定長二進制編碼,小於 128 (2^{7})的整型編碼後佔 1個字節,小於 16384 (2^{14})的整型編碼後佔 2 個,依此類推,最多可使用 10 個字節表示大於等於 2^{63} 的整型;其實現原理見下圖:markdown

image.png

編碼後的每一個字節,首位標識是否爲尾部,後續 7 位用於記錄原始數字的二進制位,舉個栗子,299 在 int32 下的二進制是微服務

00000000 00000000 00000001 00101011性能

編碼後的結果爲 10101011 00000010,即每次傳輸能夠節省 2 個字節。ui

因爲 Varint 編碼結果中每一個字節僅有 7 個位是有效位(存儲原始數據),對於小於 2^{28} 的 int32 或 int64 來講經 Varint 編碼後能夠起到壓縮的效果。固然一般狀況下小數字的使用遠遠大於大數字,所以 Varint 編碼對於大部分場景都能起到壓縮的效果。編碼

Varint 編碼實現以下:spa

const maxVarintBytes = 10 // maximum length of a varint

// EncodeVarint returns the varint encoding of x.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
// Not used by the package itself, but helpful to clients
// wishing to use the same encoding.
func EncodeVarint(x uint64) []byte {
	var buf [maxVarintBytes]byte
	var n int
	for n = 0; x > 127; n++ {
		// 首位記 1, 寫入原始數字從低位始的 7 個 bit
		buf[n] = 0x80 | uint8(x&0x7F)
		// 移走記錄過的 7 位
		x >>= 7
	}
	// 剩餘不足 7 位的部分直接以 8 位形式存下來,故首位爲 0
	buf[n] = uint8(x)
	n++
	return buf[0:n]
}
複製代碼

裏邊使用了 2 個魔數,看一下它們的二進制就能理解了,& 0x7f 取得 7 個 bit,| 0x80 將首位標記爲 1。3d

0x80 => 0000000010000000
0x7f => 0000000001111111
複製代碼

因此 Varint 的反序列化方式即是取每一個字節的後 7 位逆序拼接,以下圖(以 299 的編碼結果舉例): code

image.png

源碼以下:

// DecodeVarint reads a varint-encoded integer from the slice.
// It returns the integer and the number of bytes consumed, or
// zero if there is not enough.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
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++
		// 棄首位取 7 位並加回 x
		x |= (b & 0x7F) << shift
		// 首位爲 0
		if (b & 0x80) == 0 {
			return x, n
		}
	}

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

這裏有一個問題,EncodeVarintDecodeVarint 處理的都是 uint64 類型,若是咱們須要處理負數呢?看看直接將負數做爲 uint64 進行編碼會獲得什麼:

fmt.Println(EncodeVarint(uint64(-299)))
// output:
// [213 253 255 255 255 255 255 255 255 1]
複製代碼

結果會獲得 10 個字節的編碼,由於 uint64(-299) 的值爲 299 的補碼,須要用 64 位表示!也就是最終會獲得 varint 編碼的最大長度,官方庫中計算編碼字節長度的源碼以下:

// SizeVarint returns the varint encoding size of an integer.
func SizeVarint(x uint64) int {
	switch {
	case x < 1<<7:
		return 1
	case x < 1<<14:
		return 2
	case x < 1<<21:
		return 3
	case x < 1<<28:
		return 4
	case x < 1<<35:
		return 5
	case x < 1<<42:
		return 6
	case x < 1<<49:
		return 7
	case x < 1<<56:
		return 8
	case x < 1<<63:
		return 9
	}
	return 10
}
複製代碼

Zigzag

Zigzag 編碼將有符號整型映射到無符號整型,如其名,編碼後的值在正數與負數整型間搖擺,以下表:

Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

其實現以下:

func Zigzag64(x uint64) uint64 {
	// 左移一位 XOR (-1 / 0 的 64 位補碼)
	return (x << 1) ^ uint64(int64(x) >> 63)
}
複製代碼

這裏要注意的是若 x 爲負數,XOR 左邊爲 -x 的補碼左移一位。下圖以 -299 爲例,先計算 299 補碼,再 XOR 符號(-1 / 0)的補碼,結果爲 597;若爲正數,Zigzag 的結果爲原數的 2 倍。

image.png

小結

在寫這篇文章的時候順便複習了一波基礎知識,好比 Varint 編碼負數結果爲何是 10 個字節,緣由就是負數是以補碼的形式存儲的。因此大學裏看似入門的概念倒是實際編程中都會遇到的東西,路漫漫其修遠兮~

update: 2020.01.22 修正錯誤描述

相關文章
相關標籤/搜索