【定长编码】
变长和定长是很基本的概念,不光是在数据压缩,在其他很多地方都可以见到,这里就不多说了。
前文说过,在数据压缩时,我们需要用某些字符A替换或修改某些字符B,字符A占用的存储空间更小一些。
以数据集TOBEORNOT 为例,共出现T O B E R N六个字符,若是ASCII编码,需要8x9共72个二进制位。
在定长编码中,需要3个二进制(能区8种情况),即码字长度为3,需要3x9 = 27个二进制位,优化幅度很大。
描述不同的3位二进制对应什么字符的叫码字表,编码时将码字表写入,再一次写入每个字符的编码。读取时,先读取码字表,码字表和字符之前很好区分;字符之间可以通过固定长度区分。
可以发现,ASCII编码实际就是定长编码,给英文字母、数字、常见符号编码,用了8个二进制位。
你可以推测,我们一般用ASCII编码或其他文本编码方式保存的文本文件一定存在类似码字表的东西。
我们给的数据集的例子很小,实际上数据集中的字符个数(即长度)成千上万很正常。随着长度越来越长,出现的不同字符就会越来越多。
如果ASCII表上的大部分字符都出现过了,那么定长编码的压缩方式就很差,需要采用变长编码。
【变长编码】
变长编码(VLC,variable-length codes)会给出现频率高的字符更短的码字,这样编码后数据集的整体长度就降低。
其核心在于需要通过一套规则,给不同字符合适的码字,以确保频率高的字符有更短的码字,并使得不同码字可以互相区分。
难点在于如何从一个01的stream中区分码字,定长编码每次读取固定长度就行,变长编码不清楚每次需要读取的长度。
一种常见的思路是给定每次需要读取的长度。这种方式在数据压缩中行不通,因为长度的存储本身也要占用一定空间。
也即,不能通过太多额外的信息去确定,需要通过从stream本身已经读取或即将读取的二进制位做区分。
对数据集进行变长编码的步骤是:
- 遍历数据集中的所有字符并计算每个字符出现的频率
- 根据频率给不同字符分配码字,并建立码字表
- 再次遍历数据集根据码字表压缩数据集
VLC算法主要是关于如何生成码字的,各种各样的算法很多,需要用的时候查论文即可。但VLC不是目前主流的压缩算法,只在特定的少数场景下才会使用。
【Varint编码】
ProtoBuf中的Varint是VLC适应计算机的拓展算法,可以看到VLC的码字不按字节或字对齐,每次只读取一个二进制位,解码性能很差。
其被用来编码整数,编码时会在一个字节(=8bit)的最高位设置(MSB)为1来区分字符,如果当前读取的字节的最高位为1,那么表示需要继续读取下一位,剩下的7位用来表示该数的二进制补码。
例如,整数10可表示为 0000 1010, 整数300的二进制为1 0010 1100,补码是其自身,需要两个字节,先从低到高取7位再加上MSB为 1010 1100,另外要给编码的字节为0000 0010,合起来为300的编码1010 1100 0000 0010
正常情况下一个int类型要4个字节,而采用这种方式,对于小一些的整数,一个字节就够了,稍微大些的整数,两个字节也没问题。更大的整数会导致超出4个字节。
而在使用PB的大部分场景中,int类型整数都不会太大。
更进一步来看,所有字符都是01组合表示的,都可以当作整数来看待,都可以使用Varint编码