zoukankan      html  css  js  c++  java
  • 整数压缩编码 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编码的压缩效率就不理想了。
  • 相关阅读:
    洛谷—— P3353 在你窗外闪耀的星星
    洛谷—— P1238 走迷宫
    洛谷—— P1262 间谍网络
    9.8——模拟赛
    洛谷—— P1189 SEARCH
    算法
    May 22nd 2017 Week 21st Monday
    May 21st 2017 Week 21st Sunday
    May 20th 2017 Week 20th Saturday
    May 19th 2017 Week 20th Friday
  • 原文地址:https://www.cnblogs.com/en-heng/p/5570609.html
Copyright © 2011-2022 走看看