在有限窗口中查找重复串,并比较相邻匹配
zlib 用哈希桶和按窗口位置组织的前驱链缩小候选范围,并以搜索预算和一次前瞻在速度与压缩率之间取舍。
工程问题#
DEFLATE 要把重复字节串转换为距离与长度。逐字扫描整个历史窗口昂贵,只看一个候选又可能遗漏更好的匹配;还要决定立即使用当前匹配,还是把一个字节留为字面量后使用下一处更长匹配。
实际实现#
INSERT_STRING 把当前位置加入哈希桶,prev 数组按窗口掩码保存同桶的前驱。longest_match 沿链检查窗口内候选,受到 max_chain_length、nice_match 和最大匹配长度限制。deflate_slow 暂存前一位置的匹配;若当前位置没有更好结果,才输出前一匹配对应的距离和长度。
处理流程#
- 对短前缀计算滚动哈希,并把当前位置链接到桶头
- 沿同桶历史位置查找,排除窗口外候选并限制搜索预算
- 记录匹配长度,与相邻位置的候选比较
- 输出前一匹配或继续保留字面量,将结果交给后续编码阶段
源码片段#
固定版本 · 原文摘录159#else160#define INSERT_STRING(s, str, match_head) \161 (UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH-1)]), \162 match_head = s->prev[(str) & s->w_mask] = s->head[s->ins_h], \163 s->head[s->ins_h] = (Pos)(str))164#endifhead 保存桶头,prev 按环形窗口位置链接旧桶头;这里只产生候选。
1390local uInt longest_match(deflate_state *s, IPos cur_match) {1391 unsigned chain_length = s->max_chain_length;/* max hash chain length */1392 Bytef *scan = s->window + s->strstart; /* current string */1393 Bytef *match; /* matched string */1394 int len; /* length of current match */1395 int best_len = (int)s->prev_length; /* best match length so far */1396 int nice_match = s->nice_match; /* stop if match long enough */1397 IPos limit = s->strstart > (IPos)MAX_DIST(s) ?1398 s->strstart - (IPos)MAX_DIST(s) : NIL;1399 /* Stop when cur_match becomes <= limit. To simplify the code,1400 * we prevent matches with the string of window index 0.1401 */1402 Posf *prev = s->prev;1403 uInt wmask = s->w_mask;记录最大链探测次数、当前最好长度与历史窗口下界。
1515 if (len > best_len) {1516 s->match_start = cur_match;1517 best_len = len;1518 if (len >= nice_match) break;1519#ifdef UNALIGNED_OK1520 scan_end = *(ushf*)(scan + best_len - 1);1521#else1522 scan_end1 = scan[best_len - 1];1523 scan_end = scan[best_len];1524#endif1525 }1526 } while ((cur_match = prev[cur_match & wmask]) > limit1527 && --chain_length != 0);15281529 if ((uInt)best_len <= s->lookahead) return (uInt)best_len;1530 return s->lookahead;更新最好匹配,到足够长度或探测预算耗尽便停止;返回值限制在 lookahead 内。
1976 /* Insert the string window[strstart .. strstart + 2] in the1977 * dictionary, and set hash_head to the head of the hash chain:1978 */1979 hash_head = NIL;1980 if (s->lookahead >= MIN_MATCH) {1981 INSERT_STRING(s, s->strstart, hash_head);1982 }19831984 /* Find the longest match, discarding those <= prev_length.1985 */1986 s->prev_length = s->match_length, s->prev_match = s->match_start;1987 s->match_length = MIN_MATCH-1;19881989 if (hash_head != NIL && s->prev_length < s->max_lazy_match &&1990 s->strstart - hash_head <= MAX_DIST(s)) {1991 /* To simplify the code, we prevent matches with the string1992 * of window index 0 (in particular we have to avoid a match1993 * of the string with itself at the start of the input file).1994 */1995 s->match_length = longest_match (s, hash_head);插入当前位置,保存前一候选,并在条件允许时继续搜索。
2011 /* If there was a match at the previous step and the current2012 * match is not better, output the previous match:2013 */2014 if (s->prev_length >= MIN_MATCH && s->match_length <= s->prev_length) {2015 uInt max_insert = s->strstart + s->lookahead - MIN_MATCH;2016 /* Do not insert strings in hash table beyond this. */20172018 check_match(s, s->strstart - 1, s->prev_match, (int)s->prev_length);20192020 _tr_tally_dist(s, s->strstart - 1 - s->prev_match,2021 s->prev_length - MIN_MATCH, bflush);当前匹配没有更好时输出前一匹配的距离与长度,体现一次前瞻的选择。
摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。
收益与取舍#
工程分析带来的收益
- 哈希链避免每次遍历窗口中所有起点,prev 数组随固定窗口复用
- 有限搜索和 lazy matching 可在不做全局解析优化的情况下改善局部选择
付出的代价
- 哈希相等只表示候选,仍需真实字节比较;碰撞和重复数据会影响搜索成本
- 达到链预算或足够好的长度便停止,不保证找到窗口内最长匹配或全局最短编码
- 窗口、哈希表和链数组占用内存;此处不包含最终 Huffman 编码及块类型选择
可比较的选择
单候选哈希表减少链遍历,但更容易覆盖仍有价值的历史候选。
更完整的匹配搜索与解析优化可能改善压缩率,也会增加搜索时间、状态和内存开销。
版本与适用范围#
固定到提交 767c4c947852。分析非 FASTEST 构建中的 deflate_slow 和通用 longest_match;不同压缩级别、strategy、构建选项可选用其他路径。搜索复杂度取决于探测数和字节比较量,不能仅凭哈希表断言整体为常数时间。
参考来源#
源码、官方文档与原始论文相关实现与比较#
4 个相关案例Zstandard 的 double-fast 策略分别索引长、短前缀,在少量候选之间查验并延长真实匹配。
QEMU TCG 用 QHT 保存 TranslationBlock 索引,帮助复用与当前客体执行状态一致的翻译结果。
libvirt 为 Domain 对象维护 UUID 与名称两个哈希索引,两个入口指向同一个对象。
把上下文身份编码进块哈希,让重复请求共享已经计算的 KV cache。