在分析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总结以下: