整數壓縮編碼 ZigZag

在分析Avro源碼時,發現Avro爲了對int、long類型數據壓縮,採用Protocol Buffers的ZigZag編碼(Thrift也採用了ZigZag來壓縮整數)。html

1. 補碼編碼

爲了便於後面的分析,咱們先回顧下幾個概念:java

  • 原碼:最高位爲符號位,剩餘位表示絕對值;
  • 反碼:除符號位外,對原碼剩餘位依次取反;
  • 補碼:對於正數,補碼爲其自身;對於負數,除符號位外對原碼剩餘位依次取反而後+1。

補碼解決了原碼中\(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

  • 左移1位(n << 1),不管正數仍是負數,至關於乘以2;對於正數,若大於Integer.MAX_VALUE/2(1076741823),則會發生溢出,致使左移1位後爲負數
  • 右移31位(n >> 31),對於正數,則返回0x00000000;對於負數,則返回0xffffffff

這些性質正好在ZigZag編碼中用到了。設計

2. 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總結以下:

  1. ZigZag僅從經驗出發,認爲較小的整數會有較大的機率出現,故設計編碼策略:小整數對應的ZigZag碼字短,大整數對應的ZigZag碼字長。
  2. 可是,在特定的場景下,好比,要傳輸的整數爲大整數居多,ZigZag編碼的壓縮效率就不理想了。
相關文章
相關標籤/搜索