• 整数压缩编码 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编码的压缩效率就不理想了。
  • 相关阅读:
    Oracle 数据库简介
    Qt 中事件与处理
    npm常用命令总结
    自适应宽度布局
    原生js发送ajax请求
    微信调试本地环境代码
    多行文本溢出显示省略号
    清除浮动
    用JQuery动态为选中元素添加/删除类
    input中加入搜索图标
  • 原文地址:https://www.cnblogs.com/en-heng/p/5570609.html
Copyright © 2020-2023  润新知