您的位置:首页 > 其它

速度之王 — LZ4压缩算法

2015-08-05 16:42 597 查看
LZ4 (Extremely Fast Compression algorithm)

项目:http://code.google.com/p/lz4/

作者:Yann Collet

本文作者:zhangskd @ csdn blog

简介

LZ4 is a very fast lossless compression algorithm, providing compression speed at 400MB/s per core,

scalable with multi-cores CPU. It also features an extremely fast decoder, with speed in multiple GB/s per core,

typically reaching RAM speed limits on multi-core systems.

A high compression derivative, called LZ4_HC, is also provided. It trades CPU time for compression ratio.

(1) 横向对比

Quick comparison: single thread, Core i5-3340M @2.7GHz, using the Open-Source Benchmark by m^2 (v0.14.2)

compiled with GCC v4.6.1 on Linux Ubuntu 64-bits v11.10, using the Silesia Corpus.

对比当前流行的压缩工具,可以看到LZ4具有最快的压缩和解压速度,尽管压缩比一般。

QuickLZ官网:http://www.quicklz.com/

QuickLZ is the world’s fastest compression library, reaching 308MB/s per core.

QuickLZ自称是世界上最快的压缩算法,然而我们看到它和LZ4还是有差距的,特别是解压速度。

snappy项目:https://code.google.com/p/snappy/

snappy is developed by Google based on ideas from LZ77 and open-sourced in 2011.

It was designed to be very fast and stable, but not to achieve a high compression ratio.

Compression speed is 250MB/s and decompression speed is 500MB/s using a single threaded,

64-bit core i7 processor. The compression ratio is 20-100% lower than gzip.

snappy追求的是速度,压缩比并不高。

(2) 纵向对比

LZ4能很好的支持多线程环境,获得更高的压缩和解压速度。

(3) 技术背景

多媒体技术 -> 无损压缩 -> 词典编码 -> LZ77算法 -> LZ4

词典编码(Dictionary Encoding),根据的是数据本身包含有重复代码序列这个特性。

主要分为两类:

1. 查找正在压缩的字符序列是否在前面输入数据中出现过,如果是,则用指向早期出现过的字符串的“指针”替代

重复的字符串。这里的“词典”是指以前处理过的数据。(LZ77、LZSS)。

2. 从输入的数据中创建一个“短语词典”,编码数据过程中当遇到已经在词典中出现过的“短语”时,编码器就输

出这个词典中的短语的“索引号”,而不是短语本身。(LZ78、LZW)

(4) 其它格式

以下是一些常用的压缩格式。

1. zip

zlib库,可通过包含zlib.h使用。

zip原名为Deflate,仅支持一个LZ77的变种算法Deflate。

zip/unzip,后缀为.zip。zip也是Windows下常见的压缩格式。

2. gzip

gzip/gunzip是GNU程序,后缀为.gz。Web也常用GZIP压缩技术。

首先使用LZ77算法进行压缩,对结果再使用huffman编码进行压缩。

tar中用-z来调用:

tar -czf pic.tar.gz *.jpg

tar -xzf pic.tar.gz

3. bzip2

bzip2/bunzip2,后缀为.bz2。

相比于gzip,压缩比更高,压缩效果比传统的LZ77/LZ78更好,但压缩速度较慢。

首先使用Burrows-Wheeler变换(BWT,块排序文本压缩),然后使用哈夫曼编码进行压缩。

tar中使用-j来调用:

tar -cjf pic.tar.bz2 *.jpg

tar -xjf pic.tar.bz2

4. compress

compress/uncompress,后缀为.Z,现在已经不再流行了。

使用LZ78算法的变种LZW。

tar中使用-Z来调用。

6. rar

rar/unrar,后缀为.rar。

rar格式较zip格式的压缩比高。

注意RAR非免费,是Windows下常见压缩格式,也有RAR for Linux。

rar a pic *.jpg // pic.rar

rar e pic.rar // pic

7. 7z

7-Zip,后缀为.7z。

和rar、zip一样,7z也是Windows下常见的压缩格式。

使用改良与优化后的LZ77算法LZMA、LZMA2,压缩比高于zip。

8. xz

xz,后缀为.xz。

如果说LZ4是压缩速度之王,xz则是压缩比之王。

一般来说,用xz压缩后的文件,能比用gzip压缩的小30%,比用bzip2压缩的小15%。

主要使用LZMA2压缩算法。

tar不支持xz格式,xz / xz -d。

LZ77

我们看到很多压缩格式都是基于LZ77的,所以先来了解下LZ77算法,这里引用了较多的网上资料:)

1977年,Jacob Ziv和Abraham Lempel描述了一种基于滑动窗口缓存的技术,该缓存用于保存最近刚刚处理的文本。

LZ77编码的核心是查找从前向缓冲器开始的最长的匹配串。

压缩算法使用了两个缓存:

1. 滑动历史缓存,包含了前面处理过的N个源字符。

2. 前向缓存,包含了将要处理的下面L个字符。

压缩过程

算法尝试将前向缓存开始的两个或多个字符与滑动历史缓存中的字符串相匹配。

如果没有发现匹配,前向缓存的第一个字符作为9bit的字符输出并且移入滑动窗口,滑动窗口中最久的字符被移除。

如果找到匹配,算法继续扫描以找到最长的匹配。然后匹配字符串作为三元组输出(选项、指针和长度)。对于K个字符

的字符串,滑动窗口中最久的K个字符被移出,并且被编码的K个字符被移入窗口。

更具体来说:

1. 从当前压缩位置开始,查看未编码的数据,并试图在滑动窗口中找出最长的匹配字符串,如果找到,则进行步骤2,否则进行步骤3。

2. 输出三元符号组(off, len, c)。其中off为窗口中匹配字符串相对窗口边界的偏移,len为可匹配的长度,c为下一个字符。

然后将窗口向后滑动len + 1个字符,继续步骤1。

3. 输出三元符合组(0, 0, c)。其中c为下一个字符。然后将窗口向后滑动len + 1个字符,继续步骤1。

实例

假设窗口大小为10个字符,我们刚编码过的字符(滑动窗口)是:abcdbbccaa,即将编码的字符(前向缓存)是:abaeaaabaee。

1. 和编码字符匹配的最长串为ab,下一个字符为a,输出三元组(0, 2, a)。

窗口向后滑动3个字符为:dbbccaaaba,前向缓存为:eaaabaee。

2. 字符e在窗口中无匹配,输出三元组(0, 0, e),窗口向后滑动1个字符为:bbccaaabae,前向缓存为:aaabaee。

3. 前向缓存的最长匹配串为aaabae,下一个字符为e,输出三元组(4, 6, e),完成编码。

解压过程

解压算法必须保存解压输出的最后N个字符(滑动窗口)。

当碰到编码字符串时,使用指针和长度字段,将编码替换成实际的正文字符串。

评价

算法使用了有限的窗口在以前的文本中查找匹配,对于相对于窗口大小来说非常长的文本块,很多可能的匹配就会被丢掉。

窗口大小可以增加,但这会带来两个损失:

1. 算法的处理时间会增加,因为它必须为滑动窗口的每个位置进行一次与前向缓存的字符串匹配的工作。

2. 指针字段必须更长,以允许更长的跳转。

在多数情况下,lz77拥有较高的压缩效率。而在待压缩文件中绝大多数是些超长匹配,并且相同的超长匹配高频率地反复

出现时,lzw更具优势。GIF就是采用了lzw算法来压缩背景单一、图形简单的图片。zip是用来压缩通用文件的,这就是它

采用对大多数文件有更高压缩率的lz77算法的原因。

优化

精心设计三元组(off, len, c)中每个分量的表示方法,才能达到较好的效果。

(1) off

off为窗口内的偏移,通常的经验是,偏移接近窗口尾部的情况要多于接近窗口头部的情况,这是因为字符串在与其接近的

位置容易找到匹配串,但对于普通的窗口大小(如4096字节)来说,偏移值基本还是均匀分布的,我们完全可以用固定的位

数来表示它。

(2) len

len为字符串长度,它在大多数时候不会太大,少数情况下才会发生大字符串的匹配。显然可以使用一种变长编码方式来表

示该长度值。要输出变长的编码,该编码必须满足前缀码的条件。Huffman编码可以用于此处,但不是最好的选择。

Golomb编码应用比较广泛,对于较小的数用较短的编码,对较大的数用较大的编码表示。

(3) c

c为前向缓存中的不匹配字符。直接用8个二进制位编码。

(4) 输出格式

LZ77的原始算法采用三元组输出每一个匹配串及其后续字符,即使没有匹配,仍需要输出一个len=0的三元组来表示单个字符。

实验表明,这种方式对于某些特殊情况(例如同一字符不断重复的情形)有着较好的适应能力。

对一般数据,有一种更有效的输出方式。

将每一个输出分成匹配串和单个字符两种类型,并首先输出一个二进制位对其加以区分。例如,输出0表示下面是一个匹配串,

输出1表示下面是一个单个字符。之后,如果要输出的是单个字符,我们直接输出该字符的字节值,需要8个二进制位。

也就是说,输出一个单个的字符共需要9个二进制位。

如果要输出的是匹配串,则输出off和len。off可以用定长编码,也可以用变长前缀码。len用变长前缀码。有时候我们可以对匹配

长度加以限制,例如,限制最少匹配3个字符。因为对于2个字符的匹配串,我们使用匹配串的输出方式不一定比我们直接输出2

个单个字符(共需18位)节省空间。

这种输出方式的优点是:输出单个字符的时候比较节省空间。另外,因为不强求每次都外带一个后续字符,可以适应一些较长

匹配的情况。

(5) 查找匹配串

在滑动窗口中查找最长的匹配串,是LZ77算法中的核心问题,关系着空间和时间复杂度。

golomb

哥伦布编码。主要针对整数进行编码,对较小的数用较短的编码,对较大的数用较大的编码表示。

假设x为要进行编码的整数,当x趋于较小的取值时候,此时的Golomb编码较短,可以有效的节省空间。

压缩算法:

1. 选定参数m,b = 2^m,注意m是要在压缩前指定的。

2. q = ((x - 1) / b),表示取整。

3. r = x - qb - 1,所以x = qb + r + 1。(注意这个1不用存储,只要默认恢复出来的数据加1即可)

4. 这样要编码的x由两部分组成:

a. 第一部分由q个1加上一个0组成,表示q。

b. 第二部分由m个位组成,表示r。

恢复算法:

如果读入1,则继续往后读,直到读入0,此时读入的1的个数就是q。

之后的m位(m事先约定了)为r。

所以可以计算出x = qb + 1 + r = q * 2^m + r + 1。

Reference

[1]. http://fastcompression.blogspot.com/p/lz4.html

[2]. http://jpkc.zust.edu.cn/2007/dmt/course/MMT03_05_1.htm

[3]. http://jpkc.zust.edu.cn/2007/dmt/course/MMT03_05_2.htm

[4]. http://hi.baidu.com/guoliqiang2006/item/127c8f989b494b4ef14215db

[5]. http://blog.chinaunix.net/uid-17240700-id-3347894.html

[6]. http://jpkc.zust.edu.cn/2007/dmt/course/Mmt03_02_2.htm

[7]. http://tukaani.org/xz/

LZ4格式

The compressed block is composed of sequences.

每个数据块可以压缩成若干个序列,格式如下:

(1) literals

length of literals. If it is 0, then there is no literal. If it is 15, then we need to add some more bytes to indicate the

full length. Each additional byte then represent a value of 0 to 255, which is added to the previous value to produce

a total length. When the byte value is 255, another byte is output.

literals are uncompressed bytes, to be copied as-is.

(2) match

offset. It represents the position of the match to be copied from.

Note that 0 is an invalid value, never used. 1 means “current position - 1 byte”.

The maximum offset value is really 65535. The value is stored using “little endian” format.

matchlength. There is an baselength to apply, which is the minimum length of a match called minmatch.

This minimum is 4. As a consequence, a value of 0 means a match length of 4 bytes, and a value of 15 means a

match length of 19+ bytes. (Similar to literal length)

(3) rules

1. The last 5 bytes are always literals.

2. The last match cannot start within the last 12 bytes.

So a file within less than 13 bytes can only be represented as literals.

(4) scan strategy

a single-cell wide hash table.

Each position in the input data block gets “hashed”, using the first 4 bytes (minimatch). Then the position is stored

at the hashed position. Obviously, the smaller the table, the more collisions we get, reducing compression

effectiveness. The decoder do not care of the method used to find matches, and requires no addtional memory.

(5) Streaming format

实现

(1) 哈希表

Each position in the input data block gets “hashed”, using the first 4 bytes (minimatch). Then the position is stored

at the hashed position. Obviously, the smaller the table, the more collisions we get, reducing compression

effectiveness. The decoder do not care of the method used to find matches, and requires no addtional memory.

LZ4使用哈希表来查找匹配字符串。这个哈希表的映射关系(key, value):

key为4个字节的二进制值。

value为这4个字节在块中的位置。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

/* Default value is 14, for 16KB, which nicely fits into Intel x86 L1 cache。

* Increasing memory usage improves compression ratio

* Reduced memory usage can improve speed, due to cache effect

*/

define MEMORY_USAGE 14

define LZ4_HASHLOG (MEMORY_USAGE - 2) /* 哈希桶位数12 */

define HASHTABLESIZE (1 << MEMORY_USAGE) /* 哈希表大小2^14 = 16K */

define HASHNBCELLS4 (1 << LZ4_HASHLOG) /* 哈希桶个数2^12 = 4K */

选择哈希表的大小时,要做一个权衡:

1. 侧重压缩比,则哈希表可以大一些。

2. 侧重压缩速度,则哈希表应该适中,以便能装入L1 cache。

默认的哈希表使用的内存为16KB,能装进L1 cache,这也是LZ4压缩速度快的一个原因。

当前主流的Intel X86 L1 Data Cache为32KB,所以建议哈希表不要超过此大小。

typedef enum { byPtr, byU32, byU16} tableType_t;

哈希表存储的数据为“位置”,分三种情况:

1. inputSize小于64KB时,使用byU16,表示16位的偏移值即可。

2. inputSize大于64KB时:

2.1 指针大小为8字节,使用byU32,表示32位的偏移值,如果用指针不划算。

2.2 指针大小为4字节,使用byPtr,表示32位的指针。

采用整数哈希算法。

2654435761U是2到2^32的黄金分割素数,2654435761 / 4294967296 = 0.618033987。

计算哈希值,输入为4个字节,输出可分为2字节值、4字节值两种哈希值。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE int LZ4_hashSequence(U32 sequence, tableType_t tableType)

{

if (tableType == byU16)

/* 哈希表为16K,如果哈希value为16位 => 哈希key为13位 */

return (((sequence) * 2654435761U) >> ((MINMATCH * 8) - (LZ4_HASHLOG + 1)));

else

/* 哈希表为16K,如果哈希value为32位 => 哈希key为12位 */

return (((sequence) * 2654435761U) >> ((MINMATCH * 8) - LZ4_HASHLOG));

}

FORCE_INLINE int LZ4_hashPosition(const BYTE *p, tableType_t tableType) \

{ return LZ4_hashSequence(A32(p), tableType); }

把地址存入到哈希表中。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE void LZ4_putPositionOnHash(const BYTE *p, U32 h, void *tableBase, tableType_t tableType,

const BYTE *srcBase)

{

switch(tableType)

{

case byPtr: { const BYTE hashTable = (const BYTE ) tableBase; hashTable[h] = p; break; }

case byU32: { U32 hashTable = (U32 ) tableBase; hashTable[h] = (U32) (p - srcBase); break; }

case byU16: { U16 hashTable = (U16 ) tableBase; hashTable[h] = (U16) (p - srcBase); break; }

}

}

计算指针p指向的4字节的哈希值,然后把它的位置存入哈希表中。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE void LZ4_putPosition(const BYTE *p, void *tableBase, tableType_t tableType, const BYTE *srcBase)

{

U32 h = LZ4_hashPosition(p, tableType); /* 计算哈希值 */

LZ4_putPositionOnHash(p, h, tableBase, tableType, srcBase); /* 把地址存入哈希表 */

}

根据哈希值,获取地址。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE const BYTE *LZ4_getPositionOnHash(U32 h, void *tableBase, tableType_t tableType,

const BYTE *srcBase)

{

if (tableType == byPtr) { const BYTE hashTable = (const BYTE ) tableBase; return hashTable[h]; }

if (tableType == byU32) { U32 hashTable = (U32 ) tableBase; return hashTable[h] + srcBase; }

{ U16 hashTable = (U16 ) tableBase; return hashTable[h] + srcBase; } /* default, to ensure a return */

}

根据指针p指向的4字节,计算哈希值,并查找此哈希桶是否已有赋值。

如果此哈希桶已有赋值,则说明此时的4字节和上次的4字节很可能是一样的(如果是冲突,则是不一样的)。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE const BYTE *LZ4_getPosition(const BYTE *p, void *tableBase, tableType, const BYTE *srcBase)

{

U32 h = LZ4_hashPosition(p, tableType);

return LZ4_getPositionOnHash(h, tableBase, tableType, srcBase);

}

(2) 压缩

LZ4_compress()是压缩的一个入口函数,先申请哈希表,然后调用LZ4_compress_generic()。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

/* LZ4_compress():

* Compress inputSize bytes from source into dest.

* Destination buffer must be already allocated, and must be sized to handle worst cases situations

* (input data not compressible)

* Worst case size evaluation is provided by function LZ4_compressBound()

* inputSize: Max support value is LZ4_MAX_INPUT_VALUE

* return: the number of bytes written in buffer dest or 0 if the compression fails.

*/

int LZ4_compress(const char *source, char *dest, int inputSize)

{

if (HEAPMODE) /* 在堆中给哈希表分配内存 */

void *ctx = ALLOCATOR(HASHNBCELLS4, 4); /* Aligned on 4-bytes boundaries */


else /* 在栈中给哈希表分配内存,比较快,默认 */

U32 ctx[1U << (MEMORY_USAGE - 2) ] = {0}; /* Ensure data is aligned on 4-bytes boundaries */


endif

int result;

/* 输入小于64K+11,则用16位来表示滑动窗口,否则用32位*/
if (inputSize < (int) LZ4_64KLIMIT)
result = LZ4_compress_generic((void *)ctx, source, dest, inputSize, 0, notLimited, byU16, noPrefix);
else
result = LZ4_compress_generic((void *)ctx, source, dest, inputSize, 0, notLimited,
(sizeof(void *) == 8 ? byU32 : byPtr, noPrefix);


if (HEAPMODE)

FREE(ctx);


endif

return result;


}

[java] view plaincopy在CODE上查看代码片派生到我的代码片

define MINMATCH 4 /* 以4字节为单位查找哈希表 */

define COPYLENGTH 8

define LASTLITERALS 5

define MFLIMIT (COPYLENGTH + MINMATCH) /* 对于最后的12个字节,不进行查找匹配 */

const int LZ4_minLength = (MFLIMIT + 1); /* 一个块要>=13个字符,才会进行查找匹配 */

define LZ4_64KLIMIT ((1<<16) + (MFLIMIT - 1)) /* 64K + 11 */

/* Increasing this value will make the compression run slower on incompressible data。

* 用于控制查找匹配时的前进幅度,如果一直没找到匹配,则加大前进幅度。

*/

define SKIPSTRENGTH 6

LZ4_compress_generic()是主要的压缩函数,根据指定的参数,可以执行多种不同的压缩方案。

匹配算法

1. 当前的地址为ip,它的哈希值为h。

2. 下个地址为forwardIp,它的哈希值为forwardH (下个循环赋值给ip、h)。

3. 按照哈希值h,获取哈希表中的值ref。

3.1 ref为初始值,没有匹配,A32(ip) != A32(ref),继续。

3.2 ref不为初始值,有匹配。

3.2.1 ref不在滑动窗口内,放弃,继续。

3.2.2 ref对应的U32和ip对应的U32不一样,是冲突,继续。

3.3.3 ref在滑动窗口内,且对应的U32一样,找到了match,退出。

4. 保存ip和h的对应关系。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE int LZ4_compress_generic(void *ctx, const char *source, char *dest, int inputSize,

int maxOutputSize, limitedOutput_directive limitedOutput, tableType_t tableType,

prefix64k_directive prefix)

{

const BYTE ip = (const BYTE ) source;

/* 用作哈希表中的srcBase */

const BYTE const base = (prefix == withPrefix) ? ((LZ4_Data_Structure )ctx)->base : (const BYTE *)source);

/* 前向窗口的起始地址 */

const BYTE const lowLimit = ((prefix == withPrefix) ? ((LZ4_Data_Structure )ctx)->bufferStart : (const BYTE *)source);

const BYTE anchor = (const BYTE )source;

const BYTE const iend = ip + inputSize; / 输入的结束地址 */

const BYTE const mflimit = iend - MFLIMIT; / iend - 12,超过此处不允许再启动一次匹配 */

const BYTE const matchlimit = iend - LASTLITERALS; / iend - 5,最后5个字符不允许匹配 */

BYTE *op = (BYTE *) dest; /* 用于操作输出缓存 */
BYTE *const oend = op + maxOutputSize; /* 输出缓存的边界,如果有的话 */

int length;
const int skipStrength = SKIPSTRENGTH; /* 6 */
U32 forwardH;

/* Init conditions */
if ((U32) inputSize > (U32) LZ4_MAX_INPUT_SIZE) return 0; /* 输入长度过大 */
/* must continue from end of previous block */
if ((prefix == withPrefix) && (ip != ((LZ4_Data_Structure *)ctx)->nextBlock)) return 0;
/* do it now, due to potential early exit. 保存下一个块的起始地址 */
if (prefix == withPrefix) ((LZ4_Data_Structure *)ctx)->nextBlock = iend;
if ((tableType == byU16) && (inputSize >= LZ4_64KLIMIT)) return 0; /* Size too large (not within 64K limit) */
if (inputSize < LZ4_minlength) goto _last_literals; /* 如果输入长度小于13,则不查找匹配 */

/* First Byte */
LZ4_putPosition(ip, ctx, tableType, base); /* 计算以第一个字节开头的U32的哈希值,保存其位置 */
ip++; forwardH = LZ4_hashPosition(ip, tableType); /* 计算以第二个字节开头的U32的哈希值 */

/* Main loop,每次循环查找一个匹配,产生一个序列 */
for ( ; ; )
{
int findMatchAttempts = (1U << skipStrength) + 3;
const BYTE *forwardIp = ip;
const BYTE *ref;
BYTE *token;

/* Find a match,查找一个匹配,或者到了尽头mflimit */
do {
U32 h = forwardH; /* 当前ip对应的哈希值 */
int step = findMatchAttempts++ >> skipStrength; /* forwardIp的偏移,一般是1 */
ip = forwardIp;
forwardIp = ip + step; /* 前向缓存中下个将检查的地址 */

if unlikely(forwardIp > mflimit) { goto _last_literals; } /* >=12字节才会去匹配 */
forwardH = LZ4_hashPosition(forwardIp, tableType); /* forwardIp的哈希值 */

/* 这里是查找的关键:按照哈希值h,获取地址ref。
* 1. 没有匹配,ref为srcBase。
* 2. 有匹配。
*     2.1 不在滑动窗口内,继续。
*     2.2 对应的U32不一样,是冲突,继续。
*     2.3 在滑动窗口内,且对应的U32一样,找到了match,退出。
*/
ref = LZ4_getPositionOnHash(h, ctx, tableType, base);
LZ4_putPositionOnHash(ip, h, ctx, tableType, base); /* 保存ip、h这个对应关系 */
} while ((ref + MAX_DISTANCE < ip) || (A32(ref) != A32(ip)));

/* 找到匹配之后,看能否向前扩大匹配 */
while((ip > anchor) && (ref > lowLimit) && unlikely(ip[-1] == ref[-1])) { ip--; ref--; }

/* Encode Literal length,赋值Literal length */
length = (int) (ip - anchor);
token = op++;

/* Check output limit */
if ((limitedOutput) & unlikely(op + length + 2 + (1 + LASTLITERALS) + (length>>8) > oend)) return 0;

if (length >= (int) RUN_MASK) {
int len = length - RUN_MASK;
*token = (RUN_MASK << ML_BITS);
for(; len >= 255; len -= 255) *op++ = 255;
*op++ = (BYTE) len;
} else
*token = (BYTE) (length << ML_BITS);

/* Copy Literals,复制不可编码字符 */
{ BYTE * end = (op) + (length); LZ4_WILDCOPY(op, anchor, end); op = end; }


_next_match: /* 向后扩展匹配 */

/* Encode Offset,赋值offset,op += 2 */

LZ4_WRITE_LITTLEENDIAN_16(op, (U16) (ip - ref));

/* Start Counting */
ip += MINMATCH; ref += MINMATCH; /* MinMatch already verified */
anchor = ip;

while likely(ip < matchlimit - (STEPSIZE - 1)) {
size_t diff = AARCH(ref) ^ AARCH(ip); /* 异或,值为零表示相同 */
if (! diff) { ip += STEPSIZE; ref += STEPSIZE; continue; }
ip += LZ4_NbCommonBytes(diff); /* STEPSIZE不同,看其中有多少个字节是相同的 */
goto _endCount;
}

if (LZ4_ARCH64)
if ((ip < (matchlimit - 3)) && (A32(ref) == A32(ip))) { ip += 4; ref += 4; }
if ((ip < matchlimit - 1)) && (A16(ref) == A16(ip))) { ip += 2; ref += 2; }
if ((ip < matchlimit) && (*ref == *ip)) ip++;


_endCount:

/* Ecode MatchLength,赋值match length */

length = (int) (ip - anchor);

/* Check output limit */

if ((limitedOutput) && unlikely(op + (1 + LASTLITERALS) + (length >> 8) > oend)) return 0;

if (length >= (int) ML_MASK) {
*token += ML_MASK;
length -= ML_MASK;
for (; length > 509; length -= 510) { *op++ = 255; *op++ = 255; }
if (length >= 255) { length -= 255; *op++ = 255; }
*op++ = (BYTE) (length);
} else
*token += (BYTE) (length);

/* Test end of chunk */
if (ip > mflimit) { anchor = ip; break; } /* 不再进行匹配了 */
/* Fill table,顺便保存 */
LZ4_putPosition(ip - 2, ctx, tableType, base);

/* Test next position,尝试着找匹配 */
ref = LZ4_getPosition(ip, ctx, tableType, base);
LZ4_putPosition(ip, ctx, tableType, base);
/* 如果找到匹配,说明没有literals,可以直接跳过查找、赋值literal length */
if ((ref + MAX_DISTANCE >= ip) && (A32(ref) == A32(ip))) { token = op++; *token = 0; goto _next_match; }

/* Prepare next loop,准备进行下个循环 */
anchor = ip++;
forwardH = LZ4_hashPosition(ip, tableType);
}


_last_literals:

/* Encode Last Literals */

{

int lastRun = (int) (iend - anchor); /* 最后原字符串长度 */

if ((limitedOutput) && (((char *)op - dest) + lastRun + 1 + ((lastRun + 255 - RUN_MASK) / 255) >
(U32) maxOutputSize))
return 0; /* check output limit */

if (lastRun >= (int) RUN_MASK) {
*op ++ = (RUN_MASK << ML_BITS);
lastRun -= RUN_MASK;
for (; lastRun >= 255; lastRun -=255) *op++ = 255;
*op++ = (BYTE) lastRun;
} else
*op++ = (BYTE) (lastRun << ML_BITS);

memcpy(op, anchor, iend - anchor); /* 复制literals */
op += iend - anchor;
}

/* End */
return (int) (((char *)op) - dest); /* 返回压缩后的长度 */


}

define LZ4_MAX_INPUT_SIZE 0x7E000000 /* 2 113 929 216 bytes */

define ML_BITS 4 /* Token: 4-low-bits, match length */

define ML_MASK ((1U << ML_BITS) - 1)

define RUN_BITS (8 - ML_BITS) /* Token: 4-high-bits, literal length */

define RUN_MASK ((1U << RUN_BITS) - 1)

define MAXD_LOG 16 /* 滑动窗口的位数 */

define MAX_DISTANCE ((1 << MAXD_LOG) - 1) /* 滑动窗口的最大值 */

实现

(3) 流操作

[java] view plaincopy在CODE上查看代码片派生到我的代码片

typedef struct {

U32 hashTable[HASHNBCELLS4]; /* 哈希表 */

const BYTE bufferStart; / 类似于前向缓存 */

const BYTE base; / 哈希表中采用的基准地址srcBase */

const BYTE nextBlock; / 下一个块的地址 */

} LZ4_Data_Structure;

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE void LZ4_init(LZ4_Data_Structure *lz4ds, const BYTE *base)

{

MEM_INIT(lz4ds->hashTable, 0, sizeof(lz4ds->hashTable));

lz4ds->bufferStart = base;

lz4ds->base = base;

lz4ds->nextBlock = base;

}

创建和初始化LZ4_Data_Structure实例。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

void *LZ4_create(const char *inputBuffer)

{

void *lz4ds = ALLOCATOR(1, sizeof(LZ4_Data_Structure));

LZ4_init((LZ4_Data_Structure )lz4ds, (const BYTE )inputBuffer);

return lz4ds;

}

释放LZ4_Data_Structure实例。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

int LZ4_free(void *LZ4_Data)

{

FREEMEM(LZ4_Data);

return 0;

}

当输入缓存不够的时候,进行调整。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

char *LZ4_slideInputBuffer(void *LZ4_Data)

{

LZ4_Data_Structure lz4ds = (LZ4_Data_Structure ) LZ4_Data;

size_t delta = lz4ds->nextBlock - (lz4ds->bufferStart + 64KB); /* 调整后地址的偏移 */

if ((lz4ds->base - delta > lz4ds->base) ||                  /* underflow control */
(size_t) (lz4ds->nextBlock - (lz4ds->base) > 0xE0000000)) /* close to 32-bit limit */
{
size_t deltaLimit = (lz4ds->nextBlock - 64KB) - lz4ds->base;
int nH;
for (nH = 0; nH < HASHNBCELLS4; nH++) {
if ((size_t) (lz4ds->hashTable[nH]) < deltaLimit) lz4ds->hashTable[nH] = 0;
else lz4ds->hashTable[nH] -= (U32) deltaLimit;
}

memcpy((void *)(lz4ds->bufferStart), (const void *)(lz4ds->nextBlock - 64KB), 64KB);
lz4ds->base = lz4ds->bufferStart;
lz4ds->nextBlock = lz4ds->base + 64KB;

} else {
/* 把下个块之前的64KB数据拷贝到buffer的头部 */
memcpy((void *)(lz4ds->bufferStart), (const void *)(lz4ds->nextBlock - 64KB), 64KB);
lz4ds->nextBlock -= delta; /* 更新下个块的地址 */
/* 哈希表中的value为偏移值offset。
* pos = base + offset,现在offset -= delta,但是我们又不想去更新offset (更新哈希表)。
* 可以让base -= delta,这样可以不改变offset而取得正确的pos。
* pos是真实的地址。
*/
lz4ds->base -= delta;
}

return (char *) (lz4ds->nextBlock);


}

(4) 解压

LZ4_decompress_generic()是通用的解压算法,只要符合压缩格式就可以解压,无需考虑匹配算法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

typedef enum { endOnOutputSize = 0, endOnInputSize = 1 } endCondition_directive;

typedef enum { full = 0, partial = 1 } earlyEnd_directive;

If endOnInput == endOnInputSize, outputSize is the max size of Output Buffer.

targetOutputSize only used if partialDecoding == partial.

[java] view plaincopy在CODE上查看代码片派生到我的代码片

FORCE_INLINE int LZ4_decompress_generic( const char *source, char *dest, int inputSize,

int outputSize, int endOnInput, int prefix64k, int partialDecoding, int targetOutputSize)

{

/* Local variables */

const BYTE restrict ip = (const BYTE ) source;

const BYTE *ref;

const BYTE *const iend = ip + inputSize;

BYTE *op = (BYTE *) dest;
BYTE *const oend = op + outputSize;
BYTE *cpy;
BYTE *oexit = op + targetOutputSize;

/* static reduces speed for LZ4_compress_safe() on GCC64. */
const size_t dec32table[] = {0, 3, 2, 3, 0, 0, 0, 0};
static const size_t dec64table[] = {0, 0, 0, (size_t) - 1, 0, 1, 2, 3};

/* Special cases */
/* targetOutputSize too high => decode everything */
if ((partialDecoding) && (oexit > oend - MFLIMIT)) oexit = oend - MFLIMIT;
/* Empty output buffer */
if ((endOnInput) && unlikey(outputSize == 0)) return ((inputSize == 1) && (*ip == 0)) ? 0 : -1;
if ((! endOnInput) && unlikely(outputSize == 0)) return (*ip == 0 ? 1 : -1);

/* Main loop,每次循环解压一个序列 */
while(1) {
unsigned token;
size_t length;

/* get runlength,获取literal length */
token = *ip++;

if ((length = (token >> ML_BITS)) == RUN_MASK) {
unsigned s = 255;
while (((endOnInput) ? ip < iend : 1) && (s == 255)) {
s = *ip++;
length += s;
}
}

/* copy literals */
cpy = op + length;

if (((endOnInput) && ((cpy > (partialDecoding ? oexit : oend - MFLIMIT)) ||
(ip + length > iend - (2+1+LASTLITERALS))))
|| ((! endOnInput) && (cpy > oend - COPYLENGTH)))
{
if (partialDecoding) {
if (cpy > oend) goto _output_error; /* write attempt beyond end of output buffer */
if ((endOnInput) && (ip + length > iend)) goto _out_error; /* read attempt beyond end of input buffer */

} else {
if ((! endOnInput) && (cpy != oend)) goto _output_error; /* block decoding must stop exactly there */
if ((endOnInput) && ((ip + length != iend) || (cpy > oend))) goto _output_error; /* input must be consumed */
}

memcpy(op, ip, length);
ip += length;
op += length;
break; /* 注意,这里是退出口 */
}
LZ4_WILDCOPY(op, ip, cpy); ip -= (op - cpy); op = cpy; /* 拷贝literals */

/* get offset,获取偏移值 */
LZ4_READ_LITTLEENDIAN_16(ref, cpy, ip); ip += 2;

/* offset outside destination buffer */
if ((prefix64k == noPrefix) && unlikely(ref < (BYTE *const) dest)) goto _output_error;

/* get matchlength,获取match length */
if ((length = (token & ML_MASK)) == ML_MASK) {
/* Ensure enough bytes remain for LASTLITERALS + token */
while((! endOnInput) || (ip < iend - (LASTLITERALS + 1))) {
unsigned s = *ip++;
length += s;
if (s == 255) continue;
break;
}
}

/* copy repeated sequence,拷贝匹配match */
if unlikely((op - ref) < (int) STEPSIZE)  { /* 匹配和自身重叠的情况,拷贝STEPSIZE大小 */
const size_t dec64 = dec64table[(sizeof(void *) == 4 ? 0 : op - ref];
op[0] = ref[0]; op[1] = ref[1]; op[2] = ref[2]; op[3] = ref[3];
op += 4; ref += 4;
ref -= dec32table[op - ref];
A32(op) = A32(ref);
op += STEPSIZE - 4; ref -= dec64;
} else
LZ4_COPYSTEP(op, ref);

cpy = op + length - STEPSIZE + 4; /* match length + 4才是实际match大小*/

if unlikely(cpy > oend - COPYLENGTH - (STEPSIZE - 4)) {
if (cpy > oend - LASTLITERALS) goto _output_error; /* last 5 bytes must be literals */
LZ4_SECURECOPY(op, ref, (oend - COPYLENGTH));
while(op < cpy) *op++ = *ref++;
op = cpy;
continue;
}

LZ4_WILDCOPY(op, ref, cpy);
op = cpy; /* correction */
}

/* end of decoding */
if (endOnInput)
return (int) (((char *)op) - dest); /* Nb of output bytes decoded,解压得到了多少字符 */
else
return (int) (((char *)op) - source); /* Nb of input bytes read,读了多少个压缩字符 */


/* Overflow error detected */

_output_error:

return (int) (-(((char ) ip) - source)) - 1; / 负号表示出错,值表示Nb of input bytes read */

}

符合以下任一条件退出:

1. endOnInputSize

1.1 partial、cpy > oexit。出错情况:cpy > oend,或ip + length > iend。

1.2 full、cpy > oend - 12、ip + length == iend。出错情况:ip + length != iend,或cpy > oend。

1.3 ip + length > iend - 2 - (1 + 5)

1.3.1 partial。出错情况:cpy > oend,或ip + length > iend。

1.3.2 full、ip + length == iend。出错情况:ip + length != iend,或cpy > oend。

2. endOnOutputSize

2.1 cpy > oend - 8。

2.1.1 partial。出错情况:cpy > oend。

2.1.2 full、cpy == oend。出错情况:cpy != oend。

LZ4使用

make / make clean

得到可执行程序:lz4、lz4c

Usage:

./lz4 [arg] [input] [output]

input : a filename

Arguments :

-1 : Fast compression (default)

-9: High compression

-d : decompression (default for .lz4 extension)

-z : force compression

-f : overwrite output without prompting

-h/-H : display help/long help and exit

LZ4的输入只能为文件,不能为文件夹,毕竟一般压缩工具都不提供tar功能的。

-b file1 [file2] 可以用来测量压缩和解压速度。

比较遗憾的是,没有看可以指定线程数的参数,所以接下来没有测试多线程环境下的效果。

LZ4测试

Xeon E5504 @ 2.00GHz,X84_64,8核CPU,只用了一个。

(1) 速度

可以看到压缩速度和解压速度都很快,而且对日志文件的压缩比相当高。

(2) 压缩比

原始文件为linux-3.6.10.tar,大小为467MB。

用gzip压缩后为linux-3.6.10.tar.gz,大小为101MB,压缩比为21.62%。

用bzip2压缩后为linux-3.6.10.tar.bz2,大小为79MB,压缩比为16.91%。

用lz4压缩后为linux-3.6.10.tar.lz4,大小为166MB,压缩比为35.38%。

用lz4_HC压缩后为linux-3.6.10.tar.lz4,大小为117MB,压缩比为25.03%。

可以看到在压缩比上:lz4 < lz4_HC < gzip < bzip2。

然而在压缩过程中,笔者可以感觉到lz4的压缩时间比其它的要少一个数量级,几乎是瞬间完成:)

所以LZ4的优势在于压缩和解压速度,而不是压缩比。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: