架构师_程序员_码农网

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

搜索
查看: 4473|回复: 0

【转】LZ4 最快压缩算法解释

[复制链接]
发表于 2022-1-3 13:54:23 | 显示全部楼层 |阅读模式
看了HBO神剧《硅谷》之后一直对压缩算法很感兴趣。里面的Richard Hendricks和他的middle out压缩算法当然是假的,但是努力谷歌了一番后发现现实生活中也有这么一位压缩算法天才。

Yann Collet 在2011年发明了LZ4压缩算法。LZ4算法当然没有middle out那么牛逼得无死角,但在能保证一定压缩率的情况下,它以它无敌的压缩速度和更快的解压速度著称。

在此不再赘述对它压缩速度的测评 有兴趣可以看这篇对比文: 超链接登录可见。

另附 LZ4 的github地址: 超链接登录可见。

本文着重解释LZ4压缩算法的原理。之前有位大神写过关于LZ4算法的解释: 超链接登录可见。 但之前在学习的时候感觉这篇文章对新手不大友好, 所以另起一篇面向像我这样新手的文章。

如果一句话概括LZ4:LZ4就是一个用16k大小哈希表储存字典并简化检索的LZ77。

LZ4 是无损压缩算法,提供每核 > 500 MB/s 的压缩速度,可通过多核 CPU 进行扩展。它具有极快的解码器,每个内核的速度达数 GB/s,通常达到多核系统上的 RAM 速度限制。

速度可以动态调整,选择一个“加速”因子,该因子以压缩比换取更快的速度。另一方面,还提供了一个高压缩派生 LZ4_HC,以牺牲 CPU 时间来提高压缩率。所有版本都具有相同的解压速度。

那么LZ77又是什么呢?

LZ77压缩与原理

LZ77是一个应用了字典来进行压缩的算法。通俗来说,就是让程序观察(看字典)当前看到的数据是否和之前有重复, 如果有的话,我们就保存两个重复字段的距离(offset)和重复的长度,以替代重复的字段而以此来压缩数据。

参考超链接登录可见。

对于一串字母 A    A    B    C    B    B    A    B    C, 在读到第二个A的时候, 程序就会保存 (1,1) (距离上一个重复1位,长度为1),同理,在读到第二个B的时候,程序就会保存(2,1)(距离2,长度1)。

但是假如字符串更长些,并且程序把数据一直都记录进字典,那检索重复字段的工作就会变得异常耗时,因为最坏情况下程序每读到一个新的字母就会历遍整个字典。 LZ77使用了滑动窗口的方法来解决这个问题。

与TCP使用滑动窗口的出发点相似,滑动窗口可以缩小缓存区来避免同时处理过多缓存数据。在LZ77中,字典不是一直增加的,而是在超过字典规定的最大大小时舍弃最早加入字典的字符,以此来保证字典的大小始终维持在规定的最大大小。

假设字典长度为3

| dictionary |

| A |  A    B    C    B    B    A    B    C

输出(0,0,A)

| A    A |  B    C    B    B    A    B    C

输出(1,1)

| A    A   B |  C    B    B    A    B    C

输出(0,0,B)

A  | A    B    C |  B    B    A    B    C

输出(0,0,C)

A    A  | B    C    B |   B    A    B    C

输出(2,1)


滑动窗口的另外一部分则是待搜索缓存(look ahead buffer没有正规翻译)。 待搜索缓存就是离字典最近的还未经压缩的一部分长度。 LZ77算法会在这部分字符中匹配出与字典相同的最长字符串。上一个例子可以视作look ahead buffer 为 1。

假设字典长度为5,待搜索缓存大小为3

| dictionary | look ahead buffer |

A  | A    B    C    B    B  |  A    B    C |

输出(5,3)

匹配出的最长字符串为ABC

完整的压缩过程:

假设字典长度为5,待搜索缓存大小为3

| dictionary | look ahead buffer |

|  A    A    B  |  C    B    B    A    B    C

输出(0,0,A)

|  A  |  A    B    C  |  B    B    A    B    C

输出(1,1)

|  A    A  |  B    C    B  |  B    A    B    C

输出(0,0,B)

|  A    A   B  |  C    B    B  |  A    B    C

输出(0,0,C)

|  A   A    B    C |  B    B    A  |  B    C

输出(2,1)

|  A    A   B    C    B |   B    A    B  |  C

输出(1,1)

A  |  A    B    C    B    B  |  A    B    C  |

输出(5,3)

A    A    B    C  |  B    B    A    B    C  | 。。。 |


在压缩程序的输出文件中无需保存字典,因为解压程序会通过输出的匹配单元来还原字典。

解压过程

LZ77算法的一大优势便是它的解压非常快捷。

第一个匹配单元是 (0,0,A),则输出A。

第二个匹配单元是 (1,1),则复制输出字符串中前一位,复制长度为1,则输出A。

。。。

最后一个匹配单元是 (5,3),则复制输出字符串中往回看5位,复制长度为3,则输出A,B,C.

在LZ77算法进行压缩时,耗时最多的部分是在字典中找到待搜索缓存中最长的匹配字符。若是字典和待搜索缓存过短,则能找到匹配的几率就会很小。所以LZ4对LZ77针对匹配算法进行了改动。

首先,LZ4算法的字典是一张哈希表。 字典的key是一个4字节的字符串,每个key只对应一个槽,槽里的value是这个字符串的位置。

LZ4没有待搜索缓存, 而是每次从输入文件读入四个字节, 然后在哈希表中查找这字符串对应的槽,下文称这个字符串为现在字符串。

如果已经到最后12个字符时直接把这些字符放入输出文件。

如果槽中没有赋值,那就说明这四个字节第一次出现, 将这四个字节和位置加入哈希表, 然后继续搜索。

如果槽中有赋值,那就说明我们找到了一个匹配值。

如果槽中的值加滑动窗口的大小<现在字符的位置,那就说明匹配项位置超出了这个块的大小,那程序将哈希槽中的值更新成现在字符串的位置。

LZ4会检查一下有没有发生过hash冲突。如果槽中值所在位置的4字节与现在字符串的不相同,那一定是发生了hash冲突。作者自己编的xxhash也是以高效著称,但还是不可避免的会有冲突。遇到冲突, 程序也将哈希槽中的值更新成现在字符串的位置

最后我们能确认这个匹配项是有效的,程序会继续看匹配字符串后续的字符是不是也相同。最后复制从上一个有效匹配项结束到本次有效匹配项开始前的所有字符到压缩文件,并加上本次有效匹配项的匹配序列。

Collet 称LZ4输出的匹配单元为匹配序列(sequence),他们组成了LZ4的压缩文件。具体如下图:

QQ截图20220103135015.jpg

令牌(token)长为1字节,其中前4个字为字面长度(literal length),而其后4个字为匹配长度(match length)。前文中讲到会将上一个有效匹配项结束到本次有效匹配项开始前的所有字符复制到压缩文件,这些未经压缩的文件就是字面(literal),而他们的长度就是字面长度。

字面之后是偏差。和LZ77中偏差和匹配长度一样,偏差指的是现在字符串离它匹配项的长度,而匹配长度指的是现在字符串与字典中相同字符串的匹配长度。在解压是需要通过它来寻找需要复制的字符串和要复制的长度。偏差的大小是固定的。

图中literal length+和match length+ 是如果令牌中的字面或者匹配长度的4个字不够用了就在相应位置继续增加。4个字能表示0-15,再多的话就在增加一个字节,也就是大小加上255,直到字节中不满255。在匹配长度中,0代表4个字节。

最后12个字节在字面之后就结束了,因为它是直接被复制过去的。

我们来看下代码(python 比较抽象)


总结 说了这么多,还是没有总结一下为什么LZ4这么快。 我们首先来对比一下LZ77和LZ4对字典的检索。原生LZ77对字典的检索方式是在待搜索缓存区和字典中寻找最大匹配项。 这是一道在两个字符串中找最大相同字串的问题。 如果我们不使用动态规划,那最坏情况下就要考虑一个字符串的所有子串,然后还要在另一个字符串中进行匹配。 这样算下来,就需要O(m^2×n).如果使用动态规划,那我们则会使用一个表来保存动态的最长匹配项,而这样也只能让我们在O(m*n)的情况下完成匹配。 而且,这只是对一对搜索缓存区和字典来说。在最坏情况下, 如果没有任何匹配项,那LZ77就要算(整个文件的长度-待搜索缓存区长度)那么多道这样的匹配运算题。 而LZ4其实运用了一个更大层面上的动态规划:它将4字节与其位置保存在一个哈希表中,然后每次匹配新的4字节数据只需看哈希表中的值是不是有效。而找到有效匹配值之后看两组匹配值的后续数据时候也匹配则可以则可以找到它的最长匹配。由于LZ4的哈希表的每个key只对应1个槽,所以查找和增改哈希表的工作只需要O(1).如果在匹配后续找到了更多匹配,则需要更多组比较,但是在总时间里这些比较会代替更多的查哈希表的时间,所以LZ4算法的总时间只有O(n).不得不赞叹Collet的算法的美感啊!为了让算法的速度更上一层楼,Collet 设置默认的哈希表大小为16k,推荐不要超过32k,这样就能把它放进几乎所有cpu(intel)的一级缓存里。大家都知道cpu一级缓存的速度和内存比是天差地别的,所以LZ4的飞快速度也不足为奇,更何况LZ4的哈希表使用的哈希方程还是最快速的xxhash。 当然,这样的设计也有弊端。哈希表越小,其key也会越少。这就表示会有更多的哈希冲突发生,这是不可避免的。而更多的哈希冲突则代表了更少的匹配项。 并且哈希表越小也代表了滑动窗口也会更小,也就是说,更多的匹配项会由于距离太远而被舍弃。更少的匹配项就会代表较小的压缩比例,这就是为什么LZ4的压缩比例更不突出的原因。 展望 LZ4的应用场景非常广泛。 如《硅谷》中middle out被应用于VR中一样,LZ4由于压缩速度非常快,LZ4可以以非常小的延迟为代价带来更少的IO数量,以此来增加对带宽的利用。 还有一点点猜想,如果出了1级缓存更大的cpu,那LZ4是不是能在不损害速度的情况下提升压缩比例呢?

原文:超链接登录可见。




上一篇:TrueNAS Core 查看 snapshot(快照)位置
下一篇:Java 使用 OkHttp 发送 HTTP 网络请求
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

免责声明:
码农网所发布的一切软件、编程资料或者文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:help@itsvse.com

QQ|手机版|小黑屋|架构师 ( 鲁ICP备14021824号-2 )|网站地图

GMT+8, 2025-6-15 21:28

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表