整數壓縮編碼 ZigZag


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

1. 補碼編碼

為了便於后面的分析,我們先回顧下幾個概念:

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

補碼解決了原碼中\(0\)存在兩種編碼的問題:

\[0=[0000 \enspace 0000]_原=[1000 \enspace 0000]_原 \]

補碼\([1000 \enspace 0001]_補\) 表示\(-127\);此外,原碼中還存在加法錯誤的問題:

\[1 + (-1) = [0000 \enspace 0001]_原 + [1000 \enspace 0001]_原 = [1000 \enspace 0010]原 = -2 \]

若用補碼,則可得到正確結果:

\[ 1 + (-1) = [0000 \enspace 0001]_補 + [1111 \enspace 1111]_補 = [0000 \enspace 0000]_補 = 0 \]

因此,在計算機存儲整數時,采用的是補碼。此外,整數的補碼有一些有趣的性質:

  • 左移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編碼的壓縮效率就不理想了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM