(注:以下關(guān)于技術(shù)細(xì)節(jié)的描述是以 gzip 的公開源代碼為基礎(chǔ)的,如果需要完整的代碼,可以在 gzip 的官方網(wǎng)站 www.gzip.org下載。下面提到的每一個(gè)問題,都首先介紹最直觀簡單的解決方法,然后指出這種方法的弊端所在,最后介紹 gzip 采用的做法,這樣也許能使讀者對 gzip 看似復(fù)雜、不直觀的做法的意義有更好的理解。)
最 直觀的搜索方式是順序搜索:以待壓縮部分的第一個(gè)字節(jié)與窗口中的每一個(gè)字節(jié)依次比較,當(dāng)找到一個(gè)相等的字節(jié)時(shí),再比較后續(xù)的字節(jié)…… 遍歷了窗口后得出最長匹配。gzip 用的是被稱作“哈希表”的方法來實(shí)現(xiàn)較高效的搜索。“哈希(hash)”是分散的意思,把待搜索的數(shù)據(jù)按照字節(jié)值分散到一個(gè)個(gè)“桶”中,搜索時(shí)再根據(jù)字節(jié) 值到相應(yīng)的“桶”中去尋找。短語式壓縮的最短匹配為 3 個(gè)字節(jié),gzip 以 3 個(gè)字節(jié)的值作為哈希表的索引,但 3 個(gè)字節(jié)共有 2 的 24 次方種取值,需要 16M 個(gè)桶,桶里存放的是窗口中的位置值,窗口的大小為 32K,所以每個(gè)桶至少要有大于兩個(gè)字節(jié)的空間,哈希表將大于 32M,作為 90 年代開發(fā)的程序,這個(gè)要求是太大了,而且隨著窗口的移動,哈希表里的數(shù)據(jù)會不斷過時(shí),維護(hù)這么大的表,會降低程序的效率,gzip 定義哈希表為 2 的 15 次方(32K)個(gè)桶,并設(shè)計(jì)了一個(gè)哈希函數(shù)把 16M 種取值對應(yīng)到 32K 個(gè)桶中,不同的值被對應(yīng)到相同的桶中是不可避免的,哈希函數(shù)的任務(wù)是
1.使各種取值盡可能均勻地分布到各個(gè)桶中,避免許多不同的值集中到某些桶中,而另一些是空桶,使搜索的效率降低。
2.函數(shù)的計(jì)算盡可能地簡單,因?yàn)槊看?span lang="EN-US"> “插入”和“搜尋”哈希表都要執(zhí)行哈希函數(shù),哈希函數(shù)的復(fù)雜度直接影響程序的執(zhí)行效率,容易想到的哈希函數(shù)是取 3 個(gè)字節(jié)的左邊(或右邊)15 位二進(jìn)制值,但這樣只要左邊(或右邊)2 個(gè)字節(jié)相同,就會被放到同一個(gè)桶中,而 2 個(gè)字節(jié)相同的概率是比較高的,不符合“平均分布”的要求。
gzip 采用的算法是:A(4,5) + A(6,7,8) ^ B(1,2,3) + B(4,5) + B(6,7,8) ^ C(1,2,3) +
C(4,5,6,7,8) (說明:A 指 3 個(gè)字節(jié)中的第 1 個(gè)字節(jié),B 指第 2 個(gè)字節(jié),C 指第 3 個(gè)字節(jié),A(4,5) 指第一個(gè)字節(jié)的第 4,5 位二進(jìn)制碼,“^”是二進(jìn)制位的異或操作,“+”是“連接”而不是“加”,“^”優(yōu)先于“+”)這樣使 3 個(gè)字節(jié)都盡量“參與”到最后的結(jié)果中來,而且每個(gè)結(jié)果值 h 都等于 ((前1個(gè)h << 5)
^ c)取右 15 位,計(jì)算也還簡單。
哈希表的具體實(shí)現(xiàn)也值得探討,因?yàn)闊o法預(yù)先知道每一個(gè)“桶”會存放多少個(gè)元素,所以最簡單的,會想到用鏈表來實(shí)現(xiàn):哈希表里存放著每個(gè)桶的第一個(gè) 元素,每個(gè)元素除了存放著自身的值,還存放著一個(gè)指針,指向同一個(gè)桶中的下一個(gè)元素,可以順著指針鏈來遍歷該桶中的每一個(gè)元素,插入元素時(shí),先用哈希函數(shù)
算出該放到第幾個(gè)桶中,再把它掛到相應(yīng)鏈表的最后。
這個(gè)方案的缺點(diǎn)是頻繁地申請和釋放內(nèi)存會降低運(yùn)行速度;內(nèi)存指針的存放占據(jù)了額外的內(nèi)存開銷。
有更少內(nèi) 存開銷和更快速的方法來實(shí)現(xiàn)哈希表,并且不需要頻繁的內(nèi)存申請和釋放:gzip 在內(nèi)存中申請了兩個(gè)數(shù)組,一個(gè)叫 head[],一個(gè)叫 pre[],大小都為 32K,根據(jù)當(dāng)前位置 strstart 開始的 3 個(gè)字節(jié),用哈希函數(shù)計(jì)算出在 head[] 中的位置 ins_h,然后把 head[ins_h] 中的值記入 pre[strstart],再把當(dāng)前位置 strstart 記入 head[ins_h]。
隨著壓縮的進(jìn)行,head[]里記載著最近的可能的匹配的位置(如果有匹配的話,head[ins_h]不為 0),pre[]中的所有位置與原始數(shù)據(jù)的位置相對應(yīng),但每一個(gè)位置保存的值是前一個(gè)最近的可能的匹配的位置。
(“可能的匹配”是指哈希函數(shù)計(jì)算出的
ins_h 相同。)順著 pre[] 中的指示找下去,直到遇到
0,可以得到所有匹配在原始數(shù)據(jù)中的位置,0 表示不再有更遠(yuǎn)的匹配。
接下來很自然地要觀察 gzip 具體是如何判斷哈希表中數(shù)據(jù)的過時(shí),如何清理哈希表的,因?yàn)?span lang="EN-US"> pre[] 里只能存放 32K 個(gè)元素,所以這項(xiàng)工作是必須要做的。
gzip 從原始文件中讀出兩個(gè)窗口大小的內(nèi)容(共 64K
字節(jié))到一塊內(nèi)存中,這塊內(nèi)存也是一個(gè)數(shù)組,稱作 Window[];申請 head[]、pre[] 并清零;strstart
置為 0。
然后 gzip 邊搜索邊插入,搜索時(shí)通過計(jì)算 ins_h,檢查 head[] 中是否有匹配,如果有匹配,判斷 strstart 減 head[] 中的位置是否大于 1 個(gè)窗口的大小,如果大于 1 個(gè)窗口的大小,就不到 pre[] 中去搜索了,因?yàn)?span lang="EN-US"> pre[] 中保存的位置更遠(yuǎn)了,如果不大于,就順著 pre[] 的指示到 Window[] 中逐個(gè)匹配位置開始,逐個(gè)字節(jié)與當(dāng)前位置的數(shù)據(jù)比較,以找出最長匹配,pre[] 中的位置也要判斷是否超出一個(gè)窗口,如遇到超出一個(gè)窗口的位置或者 0 就不再找下去,找不到匹配就輸出當(dāng)前位置的單個(gè)字節(jié)到另外的內(nèi)存(輸出方法在后文中會介紹),并把 strstart 插入哈希表,strstart 遞增,如果找到了匹配,就輸出匹配位置和匹配長度這兩個(gè)數(shù)字到另外的內(nèi)存中,并把 strstart 開始的,直到 strstart + 匹配長度 為止的所有位置都插入哈希表,strstart += 匹配長度。插入哈希表的方法為:
pre[strstart % 32K] = head[ins_h];
head[ins_h] = strstart;
可 以看出,pre[] 是循環(huán)利用的,所有的位置都在一個(gè)窗口以內(nèi),但每一個(gè)位置保存的值不一定是一個(gè)窗口以內(nèi)的。
在搜索時(shí),head[] 和 pre[] 中的位置值對應(yīng)到 pre[] 時(shí)也要 % 32K。當(dāng)
Window[] 中的原始數(shù)據(jù)將要處理完畢時(shí),要把 Window[] 中后一窗的數(shù)據(jù)復(fù)制到前一窗,再讀取 32K 字節(jié)的數(shù)據(jù)到后一窗,strstart -= 32K,遍歷 head[],值小于等于 32K 的,置為 0,大于 32K 的,-= 32K;pre[] 同 head[] 一樣處理。然后同前面一樣處理新一窗的數(shù)據(jù)。
分析:現(xiàn)在可以 看到,雖然 3 個(gè)字節(jié)有 16M 種取值,但實(shí)際上一個(gè)窗口只有 32K 個(gè)取值需要插入哈希表,由于短語式重復(fù)的存在,實(shí)際只有 < 32K 種取值插入哈希表的 32K 個(gè)“桶”中,而且哈希函數(shù)又符合“平均分布”的要求,所以哈希表中實(shí)際存在的“沖突”一般不會多,對搜索效率的影響不大。可以預(yù)計(jì),在“一般情況”下,每 個(gè)“桶”中存放的數(shù)據(jù),正是我們要找的。
哈希表在各種搜索算法中,實(shí)現(xiàn)相對的比較簡單,容易理解,“平均搜索速度”最快,哈希函數(shù)的設(shè)計(jì)是搜索速度的關(guān) 鍵,只要符合“平均分布”和“計(jì)算簡單”,就常常能成為諸種搜索算法中的首選,所以哈希表是最流行的一種搜索算法。
但在某些特殊情況下,它也有缺點(diǎn),比
如:1.當(dāng)鍵碼 k 不存在時(shí),要求找出小于 k 的最大鍵碼或大于 k 的最小鍵碼,哈希表無法有效率地滿足這種要求。2.哈希表的“平均搜索速度”是建立在概率論的基礎(chǔ)上的,因?yàn)槭孪炔荒茴A(yù)知待搜索的數(shù)據(jù)集合,我們只能“信 賴”搜索速度的“平均值”,而不能“保證”搜索速度的“上限”。在同人類性命攸關(guān)的應(yīng)用中(如醫(yī)療或宇航領(lǐng)域),將是不合適的。
這些情況及其他一些特殊情
況下,我們必須求助其他“平均速度”較低,但能滿足相應(yīng)的特殊要求的算法。(見《計(jì)算機(jī)程序設(shè)計(jì)藝術(shù)》第3卷 排序與查找)。幸而“在窗口中搜索匹配字節(jié)串”不屬于特殊情況。
時(shí)間與壓縮率的平衡:
gzip 定義了幾種可供選擇的 level,越低的 level
壓縮時(shí)間越快但壓縮率越低,越高的 level 壓縮時(shí)間越慢但壓縮率越高。
不同的 level 對下面四個(gè)變量有不同的取值:
nice_length
max_chain
max_lazy
good_length
nice_length: 前面說過,搜索匹配時(shí),順著 pre[] 的指示到 Window[] 中逐個(gè)匹配位置開始,找出最長匹配,但在這過程中,如果遇到一個(gè)匹配的長度達(dá)到或超過 nice_length,就不再試圖尋找更長的匹配。最低的 level 定義 nice_length 為 8,最高的
level 定義 nice_length 為 258(即一個(gè)字節(jié)能表示的最大短語匹配長度 3 + 255)。
max_chain:這個(gè)值規(guī)定了順著 pre[] 的指示往前回溯的最大次數(shù)。最低的 level 定義 max_chain 為 4,最高的 level 定義
max_chain 為 4096。當(dāng) max_chain 和 nice_length 有沖突時(shí),以先達(dá)到的為準(zhǔn)。