前言
拖了这么长时间,终于把Xdelta3的字符串匹配解析做好了,这个部分是我们整个系列解析中最重要的部分,想要把它解释清楚说明白还真不是一件容易的事,着实费了我不少功夫。
闲话少叙,直入主题。(内容比较多,不过相信我,如果耐心看完,一定会有收获^ ^)
介绍
Xdelta3中使用的字符串匹配策略是:全局贪婪,局部懒惰。
在Xdelta3中定义了三种字符串匹配方式:RUN指令匹配
、大匹配
、小匹配
,后面将按照开销程度依次介绍。
在此之前,我们先定义几个变量:
next_in
:目标窗口的起始位置。avail_in
:目标窗口的长度。input_position
:目标窗口当前的输入位置。
假设目标窗口的数据流向是自左向右,那么在input_position
左边的数据就是已经匹配完成,被包含在生成的增量指令中的字符串,在input_position
右边的数据就是尚未匹配的字符串,也即我们每次匹配的起始位置。inp
:临时指针变量,指向目标窗口当前的输入位置,即当前尝试匹配的字符。
在每次匹配开始时inp
=next_in
+input_position
。min_match
:最短匹配长度。
即一段匹配的字符串中至少要包含min_match
个字符,这个数值是可变的,主要作用是为了避免冗余匹配。match_srcpos
:源文件中的匹配位置。
除此之外,函数内部还定义了如下变量,后续源码解析中会用到:
const int DO_SMALL = !(stream->flags & XD3_NOCOMPRESS); //如果没有禁用数据压缩功能则启用小匹配模式
const int DO_LARGE = (stream->src != NULL); //如果存在源文件则启用大匹配模式
const int DO_RUN = (1); //默认开启RUN指令匹配
const uint8_t *inp; //指向输入文件当前位置
uint32_t scksum = 0; //单个字符的校验和
uint32_t scksum_state = 0; //单个字符的32位表示
uint32_t lcksum = 0; //长度为LLOOK的字符串校验和
usize_t sinx; //小匹配哈希表的哈希索引值
usize_t linx; //大匹配哈希表的哈希索引值
uint8_t run_c; //RUN指令操作的字符
usize_t run_l; //字符run_c重复的次数
usize_t match_length; //匹配数据的长度
usize_t match_offset = 0; //将要被编码成COPY指令的匹配数据的起始偏移地址
usize_t next_move_point; //源文件校验和的计算位置
int ret; //返回值
RUN指令匹配
顾名思义,这个字符串匹配方式是针对RUN指令的,当目标文件
中某个字符连续出现多次时,我们就需要考虑其是否满足RUN
指令的匹配方式。
首先我们要先判断从当前字符inp
开始的连续min_match
个字符是否都相同,并记录字符的重复次数run_l
,若run_l
等于min_match
,则开始尝试惰性匹配,从inp[run_l]
开始依次判断后续字符是否相同,并同时更新run_l
的值;直到遇到不相同的字符或达到目标窗口结尾为止。
然后就生成一条RUN
指令到指令缓冲区中,该RUN
指令的size
就等于run_l
。但此时并不立即更新input_position
,而是将input_position
右移一位,并设置min_match
= run_l
,原因是对于不同类型增量指令
的成本来说,COPY
指令的开销是要小于RUN
指令的,所以我们更期望能找到一串更长的匹配字符串用于生成COPY
指令来覆盖这条RUN
指令所包含的数据,因此最短匹配长度min_match
也需要更新为run_l
。
大匹配
这个字符串匹配方式是发生在源文件
中的,所以使用该匹配方式的首要前提就是要包含源文件
。
一般来说,字符串匹配都是从一个匹配的字符开始,但在实际情况下,我们不可能每编码一个目标文件
的字符就从头到尾检索一遍源文件
,这样做的效率是非常低的。因此在Xdelta3中,使用了滑动匹配窗口的匹配算法,它可以使每次匹配检索一段字符串,而非一个字符,从而大幅度提高匹配效率。
滑动匹配窗口
这个算法执行在字符串匹配之前,滑动匹配窗口的作用是计算被窗口包含的字符串的校验和,并记录此时匹配窗口的起始位置;每计算一次字符串校验和后,匹配窗口滑动large_step
个字节单位;滑动是由尾部向头部(自右向左)的方向进行,直到窗口的下一次滑动将超过源文件
起始位置为止。
滑动匹配窗口的大小为large_look
,即每次计算校验和的字符串长度;匹配窗口的大小large_look
和滑动距离large_step
共同决定了大匹配方式的匹配效率,在Xdelta3中内设了五种字符串匹配的参数配置,其中对large_look
和large_step
配置的值分别为:
large_look | large_step | |
---|---|---|
DEFAULT | 9 | 3 |
SLOW | 9 | 2 |
FAST | 9 | 8 |
FASTER | 9 | 15 |
FASTEST | 9 | 26 |
Xdelta3也支持用户自定义字符串匹配参数配置,下面我们以默认配置为例,来进一步了解滑动匹配窗口算法:
如图所示,匹配窗口从源文件
末尾开始,包含长度为large_look
个字节的字符串,每次向前滑动large_step
个字节,直到源文件
的起始位置。
计算源窗口校验和的函数源码解析:
//使用滑动匹配窗口算法推进源文件数据校验和的计算,若计算完所有源数据,则next_move_point = USIZE_T_MAX,否则next_move_point指向下一次计算校验和的位置。
static int xd3_srcwin_move_point(xd3_stream *stream, usize_t *next_move_point)
{
xoff_t target_cksum_pos; //源校验和的目标计算位置
xoff_t absolute_input_pos; //目标文件的绝对输入位置(加上目标文件的起始偏移量)
//当源文件的长度是已知时
if (stream->src->eof_known)
{
xoff_t source_size = xd3_source_eof(stream->src); //获取整个源文件的长度
//stream->srcwin_cksum_pos是已计算校验和的源文件位置
if (stream->srcwin_cksum_pos == source_size)
{
*next_move_point = USIZE_T_MAX;
return 0;
}
}
absolute_input_pos = stream->total_in + stream->input_position;
//确定要计算校验和的源数据范围
if (absolute_input_pos < stream->src->max_winsize / 2)
{
target_cksum_pos = stream->src->max_winsize;
}
else
{
target_cksum_pos = absolute_input_pos + stream->src->max_winsize / 2 + stream->src->blksize * 2;
target_cksum_pos &= ~stream->src->maskby;
}
//当匹配的字符串已经越过了上一次最后计算源校验和的位置时,不计算已经匹配的源数据校验和
if (stream->maxsrcaddr > stream->srcwin_cksum_pos)
{
stream->srcwin_cksum_pos = stream->maxsrcaddr;
}
if (target_cksum_pos < stream->srcwin_cksum_pos)
{
target_cksum_pos = stream->srcwin_cksum_pos;
}
//开始计算源校验和,源文件通常比较大,因此会被分割成多个源窗口,每个源窗口也被称为源块
while (stream->srcwin_cksum_pos < target_cksum_pos && (!stream->src->eof_known || stream->srcwin_cksum_pos < xd3_source_eof(stream->src)))
{
xoff_t blkno; //当前计算校验和的源窗口块号
xoff_t blkbaseoffset; //当前计算校验和的源窗口起始偏移地址
usize_t blkrem; //用于暂存已计算过校验和的源位置
ssize_t oldpos; //已计算过校验和的源位置
ssize_t blkpos; //滑动匹配窗口的起始位置(从源窗口的尾部向首部滑动): do { blkpos-- } while (blkpos >= oldpos);
int ret;
//xd3_blksize_div函数计算stream->srcwin_cksum_pos对应的源窗口块号blkno,并计算该地址在块内的偏移位置blkrem。
xd3_blksize_div(stream->srcwin_cksum_pos, stream->src, &blkno, &blkrem);
oldpos = blkrem;
//xd3_getblk函数用于获取源窗口块号为blkno的相关信息,成功返回0
if ((ret = xd3_getblk(stream, blkno)))
{
if (ret == XD3_TOOFARBACK)
{
ret = XD3_INTERNAL;
}
return ret;
}
blkpos = xd3_bytes_on_srcblk(stream->src, blkno); //xd3_bytes_on_srcblk函数返回块号blkno的长度
//如果滑动匹配窗口越过源窗口的起始位置,则表示已计算完当前源窗口的校验和
if (blkpos < (ssize_t)stream->smatcher.large_look)
{
stream->srcwin_cksum_pos = (blkno + 1) * stream->src->blksize;
continue;
}
blkpos -= stream->smatcher.large_look;
blkbaseoffset = stream->src->blksize * blkno;
/* 从源窗口的末尾开始计算滑动匹配窗口内数据的校验和,每次计算large_look个字节,每次计算后匹配窗口向块首滑动large_step个字节;
* 并将每次计算得到的校验和通过hash函数计算出哈希表large_table的索引值hval,large_table[hval] = (滑动匹配窗口的起始地址 + HASH_CKOFFSET) */
do
{
uint32_t cksum = xd3_lcksum(stream->src->curblk + blkpos, stream->smatcher.