gzip内幕

来源:互联网 发布:淘宝自动签收时间 编辑:程序博客网 时间:2024/04/30 12:04

轮廓

        gzip压缩的核心思想有两个,一是指代重复的内容,二是哈夫曼编码。指代重复内容,就是把重复出现的内容用二元组(distance,length)替代。distance是指相互匹配的两块内容之间的距离,length是指匹配的长度。完成了指代重复内容的工作后,原始数据就被分成了三类:literal(没被匹配的字节)、distance和length,接下来就对它们进行哈夫曼编码了。最后把输入文件的编码发送到压缩文件中就可以了。


寻找重复的内容

        要替换掉重复的内容,首先就要把重复的内容找到。怎么去找呢?

         Gzip采用的方法类似于Rabin-Karp字符串匹配算法。

         R-K算法的基本思路是这样的:1、计算模式串的哈希值;2、依次计算文本串中各个子串(仅限于长度与模式串相同的那些子串)的哈希值,如果与模式串的哈希值相同就把这个子串与模式串作进一步比较,看是否真的匹配。

        在gzip中,所谓的模式串就是以指针strstart所指为起点长度为3字节的串(strstart指示当前处理到输入文件的哪个位置),而要被匹配的文本串则是strstart之前的内容。当模式串在得到了自己的哈希值后,就把自身的位置存储到哈希表里,顺便看看以前有没有在某个地方得到过相同的哈希值,如果有就去仔细比较。这样如果以前出现过相同的串,就可以比较快捷地找到了。之后,strstart指针向后移一位,指向下一个字节,对新模式串进行同样的操作。

        下面具体展开。哈希表依靠两个数组实现:head和prev,prev数组能形成哈希冲突链。这里没有用到指针,仅靠一个数组却能把冲突链串起来,怎么做到的呢?举例来说明吧,假如已经有head[8]=10,说明最近插入的一个哈希值是8的串的位置在10处。假设现在计算出一个模式串的哈希值恰好又是8,而这个模式串的位置是20,就把20插入到哈希表相应的表项中,插入操作就是几个赋值语句,形成了head[8]=20,prev[20]=10。如果待会儿又有位置为30的模式串计算出哈希值为8,则哈希表就变为head[8]=30,prev[30]=20,prev[20]=10。再结合head与prev(previous)本身的意思,我们就可以看出了,head和prev中的元素本身额外地充当了指针的功能,所以能形成链。这样实现的好处除了节省空间外,还可隐性地删除链中结点,只要对prev的某个元素重新赋值,立刻该元素就从一条链删除,而插入到了另一条链中去了,这样就神不知鬼不觉地去掉了过旧的冲突。

       在插入到哈希表之后,我们可以开始寻找重复内容了,顺着prev的指点,从近往远依次比较那些“疑似”匹配串,挑最长匹配串作为最后结果,然后把模式串以(distance,length)的形式存储起来。如果模式串发现自己是第一个到某个head元素处报到的(即该head元素的值为空),那么它就没希望被匹配上,直接被作为literal存储。有的模式串很不幸,顺着prev找了一遍后却没找到匹配,那么它第一个字节也会被作为literal存储。

        依靠标志数组flag_buf,literal、distance、length这三类数据是被有序地记录了下来的,所以待会儿就可以有序地把它们的编码发送到压缩文件中。

 

哈夫曼编码

        在存储literallengthdistance时,gzip统计这些数据(结点)的频率。每个类型所有树结点被存放在一个相应的类型为ct_data的数组中,ct_data的定义如下:

        freqdad域在构造树的时候用到,而codelen域在生成编码时用到:用两个共用体表示,节省空间,互不冲突。在这个结构体中我们没有看到关于结点的信息,比如这个结点表示字符‘a’,我们拿什么表示呢?原来,结点的信息存放在数组的下标中,‘a’结点就是数组的第97个元素(97是‘a’ASCII码)。

        我们在课本里学到的哈夫曼编码是从哈夫曼树的根节点出发,向左拐编码为0,向右拐编码为1,总之是要“钻”到树里,给每个结点一比特一比特地编码。但gzip做的很精巧,省去了这些费时的麻烦。

        gzip根据各结点的编码长度(如何得到:本结点的编码长度是其父结点长度+1,根结点的编码长度为1,迭代可得到所有结点的编码长度),再得到每种编码长度各有多少结点,进而得到各种长度的结点的起始编码(按编码长度从小到大迭代处理,本编码长度的起始编码,等于上一个编码长度的起始编码+上一个编码长度的结点数,再左移一位),将其存放在数组next_code中,给结点编码时,当用某个next_code元素给一个相应编码长度的结点赋编码后,该next_code元素自加1,为下一个同样编码长度的结点作好准备。所有结点的编码一气呵成。这个方法巧妙地利用了结点以及结点的顺序都是固定的这一事实。(说得有些乱,看源码则一目了然)

         所以,我们仅仅依靠各结点的编码长度就完成了编码,反过来说,我们只要在压缩文件中额外存放各结点的编码长度,就可以推算出各结点的编码了。所以,把各结点的编码长度依次直接发送到压缩文件中就可以了,但gzip觉得这样会太费空间,就把编码长度再加以哈夫曼编码。这样要恢复出整棵哈夫曼树,只要在压缩文件中放编码长度的编码就可以了。

        有必要提一下给编码长度编码的过程。用同样的数据结构ct_data收集编码长度的频率信息,但gzip做了一些改进。为了说明改进的理由先给一组数据:为literal、length建哈夫曼树(两者是放在一起编码的)时结点最多有286个,而编码长度的最大值是15。也就是说这286个编码长度的可取值只有0到15这16个整数。我们要有序地把每个结点的编码长度存放在压缩文件中,那么有很大的可能性会连续出现相同的长度,如39 0 0 0 8 7 7 7 7 7......这里连续出现了3个0,5个7。对于这种情况gzip采用“游程编码”来进一步压缩数据。通过额外增加REP_3_6、REPZ_3_10REPZ_11_138这三个结点,来分别表示重复前一个值36次,重复零310次,重复零11138次。这三个结点跟普通结点混在一起被编码。最终,00 0在压缩文件中表示为code(REPZ_3_10) 00明确了重复次数(30)次,而最后57被表示成code(7)code(REP_3_7) 11明确了重复(31)次,否则的话,00 077 7 7 7会被表示成code(0)code(0) code(0)以及code(7)code(7) code(7) code(7) code(7)code(7),其中code(x)代表x的编码。可见,游程编码起了一定的压缩作用。

 

向压缩文件发送原文件的编码

        需要澄清一点,gzip并不对length和distance这两类数据直接进行哈夫曼编码。因为直接编码涉及的结点数太多了,时间开销可能会很大。gzip只是取它们二进制表示的高若干位编码,低位不参与编码,所以实际参与编码的length只有29个结点,distance只有30个结点。这样在向压缩文件发送编码时,把原数据的高若干位用编码顶替,而低位原样发送。这样做固然不利于提高压缩率,但对提高程序的速度是大有裨益的。

        其实gzip中除了动态哈夫曼编码外,还有静态的哈夫曼编码,以及裸拷贝。在工作过程中,是以块(block)为单位压缩的,一个文件可能被分为若干块而分别被压缩,哪种方法压缩效果最好,就取哪种方法。

        总结一下,如果采用动态哈夫曼编码,压缩文件中将会有这三类相互之间有层次关系数据:A、编码长度的编码(有序地排列);B、各种结点的编码长度(用A中的相应数据表示);C、原输入文件的编码(用B中的相应数据表示)。

        如果用静态哈夫曼编码,因为编码永远固定,所以少了A、B两类数据,但由于静态编码是盲目的,压缩效果肯定不如动态编码,所以谁更胜一筹就不好说了,比了才知道。


相关实现

*与压缩有关的代码几乎都在deflate.c和trees.c这两个文件中。

*“主函数”是deflate() / deflate_fast( )。其中deflate()用延迟匹配技术(lazymatch)做了优化。

*计算模式串哈希值的哈希函数:

        #defineUPDATE_HASH(h,c) (h = (((h)<<H_SHIFT) ^ (c)) & HASH_MASK)

       其中的h就是哈希值。

*pqdownheap()维护优先队列(pqpriorityqueue)。在构建哈夫曼树时,从优先队列中取出频率最小的两个结点,作其父结点插入优先队列(但只做两次维护操作:)

*extra_lbits[]extra_dbits[]分别规定了lengthdistance的二进制表示的低多少位不参与哈夫曼编码。由此可推出怎样把length映射成29个结点,把distance映射成30个结点。

。。。。。。


后记

       强烈推荐刚学编程的朋友们阅读一下gzip,起点不高,只要学过数据结构就可以了。

感受一下其优雅、轻盈的代码(尤其是哈夫曼编码这一段),学习一下其明晰的命名、注释(不然我怎能挖出这些好东西),一定会受益匪浅的。

gzip写的年代较早,用的是旧式的C语言风格,但不碍阅读。阅读工具我用ctagsgvim,感觉不错:)


参考:http://blog.csdn.net/imquestion/archive/2004/03/15/16439.aspx