增量压缩工具Xdelta3源码解析——字符串匹配

本文深入解析Xdelta3的字符串匹配策略,包括RUN指令匹配、大匹配和小匹配。重点阐述滑动匹配窗口、大匹配哈希表及其匹配流程。文章详细介绍了匹配算法的核心代码,帮助读者理解Xdelta3如何实现高效低成本的字符串匹配。
摘要由CSDN通过智能技术生成

前言

拖了这么长时间,终于把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_looklarge_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.
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值