在分析Avro源碼時,發現Avro爲了對int、long類型數據壓縮,採用Protocol Buffers的ZigZag編碼(Thrift也採用了ZigZag來壓縮整數)。html
爲了便於後面的分析,咱們先回顧下幾個概念:java
補碼解決了原碼中\(0\)存在兩種編碼的問題:apache
\[ 0=[0000 \enspace 0000]_原=[1000 \enspace 0000]_原 \]函數
補碼\([1000 \enspace 0001]_補\) 表示\(-128\);此外,原碼中還存在加法錯誤的問題:oop
\[ 1 + (-1) = [0000 \enspace 0001]_原 + [1000 \enspace 0001]_原 = [1000 \enspace 0010]原 = -2 \]this
若用補碼,則可獲得正確結果:google
\[ 1 + (-1) = [0000 \enspace 0001]_補 + [1111 \enspace 1111]_補 = [0000 \enspace 0000]_補 = 0 \]編碼
所以,在計算機存儲整數時,採用的是補碼。此外,整數的補碼有一些有趣的性質:spa
Integer.MAX_VALUE/2
(1076741823),則會發生溢出,致使左移1位後爲負數0x00000000
;對於負數,則返回0xffffffff
這些性質正好在ZigZag編碼中用到了。設計
對於int值1,-1,20151103,均是用4 Bytes來表示:
\[ 1 = [00 \enspace 00 \enspace 00 \enspace 01] \\ -1 = [ff \enspace ff \enspace ff \enspace ff] \\ 20151103 = [01 \enspace 33 \enspace 7b \enspace 3f] \]
在《Huffman編碼》中證實了壓縮編碼應知足:
高几率的碼字字長應不長於低機率的碼字字長
通常狀況下,使用較多的是小整數,那麼較小的整數應使用更少的byte來編碼。基於此思想,ZigZag被提出來。
首先,ZigZag按絕對值升序排列,將整數hash成遞增的32位bit流,其hash函數爲h(n) = (n << 1) ^ (n >> 31)
;對應地long類型(64位)的hash函數爲(n << 1) ^ (n >> 63)
。整數的補碼(十六進制)與hash函數的對應關係以下:
n | hex | h(n) | ZigZag (hex) |
---|---|---|---|
0 | 00 00 00 00 | 00 00 00 00 | 00 |
-1 | ff ff ff ff | 00 00 00 01 | 01 |
1 | 00 00 00 01 | 00 00 00 02 | 02 |
-2 | ff ff ff fe | 00 00 00 03 | 03 |
2 | 00 00 00 02 | 00 00 00 04 | 04 |
... | ... | ... | ... |
-64 | ff ff ff c0 | 00 00 00 7f | 7f |
64 | 00 00 00 40 | 00 00 00 80 | 80 01 |
... | ... | ... | ... |
拿到hash值後,想固然的編碼策略:直接去掉hash值的前導0以後的byte做爲壓縮編碼。可是,爲何ZigZag(64)=8001
呢?這涉及到編碼惟一可譯性的問題,只有當編碼爲前綴碼才能保證可譯,即
任意一碼字均不爲其餘碼字的前綴
咱們來看看,若是按上面的策略作壓縮編碼,則
h(0) = 0x0 = [00] h(64) = 0x80 = [80] h(16384) = 0x8000 = [80 00]
那麼,當收到字節流[80 00]
時,是應解碼爲兩個整數64, 00
,仍是一個整數16384
?所以,爲了保證編碼的惟一可譯性,須要對hash值進行前綴碼編碼,ZigZag採用了以下策略:
input: int n output: byte[] buf loop if 第七位滿1或有進位: n |= 0x80; 取低位的8位做爲一個byte寫入buf; n >>>=7(無符號右移7位,在高位插0); else: 取低位的8位做爲一個byte寫入buf end
ZigZag編碼的Java實現(從org.apache.avro.io.BinaryData
摳出來的):
/** Encode an integer to the byte array at the given position. Will throw * IndexOutOfBounds if it overflows. Users should ensure that there are at * least 5 bytes left in the buffer before calling this method. * @return The number of bytes written to the buffer, between 1 and 5. */ public static int encodeInt(int n, byte[] buf, int pos) { // move sign to low-order bit, and flip others if negative n = (n << 1) ^ (n >> 31); int start = pos; if ((n & ~0x7F) != 0) { buf[pos++] = (byte)((n | 0x80) & 0xFF); n >>>= 7; if (n > 0x7F) { buf[pos++] = (byte)((n | 0x80) & 0xFF); n >>>= 7; if (n > 0x7F) { buf[pos++] = (byte)((n | 0x80) & 0xFF); n >>>= 7; if (n > 0x7F) { buf[pos++] = (byte)((n | 0x80) & 0xFF); n >>>= 7; } } } } buf[pos++] = (byte) n; return pos - start; }
ZigZag是一種變長編碼,當整數值較大時,hash值的十六進制的有效位會較長,對應地ZigZag碼字會出現須要5 byte存儲;好比,
ZigZag(Integer.MAX_VALUE)=[fe ff ff ff 0f]
解碼爲編碼的逆操做,首先,將ZigZag編碼還原成hash值,而後用hash函數\(h(n)\)的逆函數\(h^{-1}(n)\) = (n >>> 1) ^ -(n & 1)
獲得原始的整數值。Java代碼實現(在avro源碼org.apache.avro.io.BinaryDecoder
中)以下:
public static int readInt(byte[] buf, int pos) throws IOException { int len = 1; int b = buf[pos] & 0xff; int n = b & 0x7f; if (b > 0x7f) { b = buf[pos + len++] & 0xff; n ^= (b & 0x7f) << 7; if (b > 0x7f) { b = buf[pos + len++] & 0xff; n ^= (b & 0x7f) << 14; if (b > 0x7f) { b = buf[pos + len++] & 0xff; n ^= (b & 0x7f) << 21; if (b > 0x7f) { b = buf[pos + len++] & 0xff; n ^= (b & 0x7f) << 28; if (b > 0x7f) { throw new IOException("Invalid int encoding"); } } } } } pos += len; return (n >>> 1) ^ -(n & 1); // back to two's-complement }
ZigZag總結以下: