编码与压缩:
下次可以买来看的书:
- https://book.douban.com/subject/25789553/ 数据压缩导论(第4版)
- https://book.douban.com/subject/35034359/ 数据压缩入门
压缩算法一般根据应用场景不同可分为文本压缩和索引压缩,后者是搜索引擎的核心技术之一。压缩的本质是对数据进行重新编码,编码依据是数据的分布特性,一般是概率分布情况,比如对于倒排索引中的数字编号。数值分布一般符合指数分布,均匀分布,Elias Gamma/Delta主要是基于这两种分布的。
压缩模型可分为全局模型,局部模型,局部模型参数可动态调整,算法复杂度高,压缩解压缩速度慢,但压缩率高。在编码方式上可分为定长和变长编码。
Elias Gamma/Elias Delta算法适合小整数使用频率较高的场景。它们都是基于数字分解的算法
- variable byte
- gamma
- zigzag
variable byte (在lucene中就是 VInt)
将一个数据拆分为多个编码单位存储, 读的时候读若干个连续的编码单元组成一个数据, 如用一个字节做编码单位, 这个字节的最高位为0表示需要继续读取数据, 另外7个bit表示真实的数据, 需要和之后读取的数据结合起来使用, 当读到的字节的最高位为1, 表示是最后一个编码单元了, 可以将之前读到的数据结合在一起. 在实际中, 也可能用1做延续位标识, 用0作结束标识.
编码单位越大, 操作次数越小, 即解压缩越快, 但是压缩率低. 以8位, 一个字节作为编码单位, 是压缩率与解压缩速度的权衡.
通过1-5个字节来压缩四个字节的数据类型, 如int, 对于数值较小的int具有较好的压缩率, 对于较大的int, 或者负数(最高位-1), 压缩率较低.
例子: 以 0 做延续位标识.
static List encodeNumber(int num) {
// byte i = (byte) 0x80;
List bytes = new LinkedList<>();
while (true) {
Byte b = (byte) (num & 0x7F); // 获取数据低位
bytes.add(b); // 写入, 数据低位在索引低位
if ((num >>>= 7) == 0) {
break;
}
}
int lastOne = bytes.size() - 1;
Byte aByte = bytes.get(lastOne);
Byte last = (byte) (aByte + 128);
bytes.set(lastOne, last);
return bytes;
}
static List encodeNumbers(int... nums) {
List bytes = new LinkedList<>();
for (int num : nums) {
bytes.addAll(encodeNumber(num));
}
return bytes;
}
static int[] decodeBytes(List bytes) {
List ints = new LinkedList<>();
int v = 0;
int count = 0;
// 先获取的是数据低位
for (Byte b : bytes) {
var flag = b & 0x80; // 1000 0000
var ans = b & 0x7F;
ans <<= (count++)*7;
v |= ans;
if (flag != 0) {
ints.add(v);
count = 0;
v = 0;
} else {
}
}
return ints.stream().mapToInt(Integer::intValue).toArray();
}
lucene中的writeVInt 实现
DataOutput
是以1作为延续位标识
public final void writeVInt(int i) throws IOException { // java 内存是小端吗? 如果把低位数据认为是低地址,那就是小端吧
while ((i & ~0x7F) != 0) { // i & 1111...1111 1000 0000 , res != 0 表示: 如果除了低7位的其他位还有数据
writeByte((byte) ((i & 0x7F) | 0x80)); // i & 0111 1111 , 取七位, | 1000 0000, 或1是为了设置连续位
i >>>= 7; // 数据低位最先被写入
} // 跳出循环,表示除了低七位的其他高位没有数据了,可以直接写入i了,i里面的第八位肯定是0
writeByte((byte) i); // 对于负数也是支持的, 因为负数的标识-1在32位, 我们用了40位, 32的信息不会被扰乱
}
long的写入也很类似
private void writeSignedVLong(long i) throws IOException {
while ((i & ~0x7FL) != 0L) {
writeByte((byte) ((i & 0x7FL) | 0x80L));
i >>>= 7;
}
writeByte((byte) i);
}
gamma
首先了解一元编码:
主要适合于指数分布的数值分布,对于数字N,一元编码的编码长度为N,使用N-1个二进制1和末尾一个0表示,如数字3的一元编码为:110
gamma, elias gamma encoding: 参考:https://tonymazn.wordpress.com/2018/09/03/%E5%8E%8B%E7%BC%A9%E7%AE%97%E6%B3%95%E4%B9%8Belias-gamma-coding-elias-delta-coding/
- 对于数字x分解成 x=2N + M
- 对于N+1使用一元编码
- 对于M使用比特宽度为N的二进制编码
举例:比如对于数字13,可分解成:13 =23 + 5,即N=3,M=5,则N+1的一元编码为:1110;M的比特宽为3的二进制编码为:101,最后的Elias Gamma Coding为:1110:101.
而 elias Delta Coding是将Gamma coding的第一个因子再进行了一次Gamma coding。
zigzag
zigzag编码主要是为了应对 variable bytes的缺点: 对负数的压缩率不高的问题,因为负数采用的补码,比如-6
0000 0110
-> 补码 1111 1111 1111 1111 1111 1111 1111 1001
-> 加1 1111 1111 1111 1111 1111 1111 1111 1010
可见, 为了编码,负数,还要比int多用一个字节。 而zigzag编码则可以解决, zigzag本身很简单:
- a=原数向低位移动直到只剩下最后一位,高位用1补全
- b=原数向左移动1位,低位用0补全
- a和b进行异或操作: a^b
public static int zigZagEncode(int i) {
return (i >> 31) ^ (i << 1);
}
/**
* Zig-zag encode
* the provided long. Assuming the input is a signed long whose absolute value can be stored on
* n
bits, the returned value will be an unsigned long that can be stored on
* n+1
bits.
*/
public static long zigZagEncode(long l) {
return (l >> 63) ^ (l << 1);
}
经过zigzag之后, 再用variable byte, 只需要一个字节就够了。