概述
lz4算法是lz77算法的一种实现,就是查找重复的字符串,重复的字符串使用(距离,长度)来表示。
比如abcdefgabcdefg,被压缩后就表示成了:
abcdefg,(1,7)
距离有多重表示方法:
- 重复的字符串尾部距离当前正在处理的字符的距离
- 重复的字符串头部距离当前正在处理的字符的距离
- 重复的字符串在输入字符串的位置
- 重复字符串头部位置的内存地址
lz77更详细的资料,可以查看其它博文,本文主要讲lz4中的实现。
lz4的实现
数据结构(hash)
LZ4最小匹配长度设的是4个字节,只有重复的字符串长度大于等于4,才认为是找到了重复字符串。
LZ4使用了hashtable,来存储之前出现过的字符串(4个字节)。hashtable实际上就是java或c++中的Map。
key值的计算
LZ4_FORCE_INLINE void LZ4_putPosition(const BYTE* p, void* tableBase, tableType_t tableType, const BYTE* srcBase)
{
U32 const h = LZ4_hashPosition(p, tableType);
LZ4_putPositionOnHash(p, h, tableBase, tableType, srcBase);
}
LZ4_hashPosition函数就是计算key值,是使用p指针所指向的4个字节的值来计算的,代表4个byte的值。
LZ4_putPositionOnHash函数就是向hashtable中存入value,value是代表的是字节的位置。
value的存储
hashtable中的value代表的是字节的位置,字节的位置有两种表示方式:
1.字节的绝对地址(指针)
2.字节在数据中的索引。
LZ4按最小表示长度来选择的,这样hashtable就可以存储更多的值,降低哈希碰撞
if (maxOutputSize >= LZ4_compressBound(inputSize)) {
if (inputSize < LZ4_64Klimit) {
return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, byU16, noDict, noDictIssue, acceleration);
} else {
const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32;
return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, 0, notLimited, tableType, noDict, noDictIssue, acceleration);
}
} else {
if (inputSize < LZ4_64Klimit) {
return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration);
} else {
const tableType_t tableType = ((sizeof(void*)==4) && ((uptrval)source > LZ4_DISTANCE_MAX)) ? byPtr : byU32;
return LZ4_compress_generic(ctx, source, dest, inputSize, NULL, maxOutputSize, limitedOutput, tableType, noDict, noDictIssue, acceleration);
}
}
- src的长度小于64K,这样可以用16位表示,因此使用byU16策略,就是使用16位(short)存储数组索引
- 如果src的长度大于64K且指针类型的长度是4个字节(byPtr),则直接存储字节的地址
备注:因为存在hash碰撞,所以通过key值获取到字节后,还要进行值的对比。这里如果字节存储指针,到时候进行值的对比的时候,会少一个计算。 - 如果src的长度大约64K,且指针类型的长度不为4个字节(不为4个字节,就肯定大约4个字节了,不然src的长度,不可能大于64K),这直接存储32位数组索引(byU32)。
LZ4_FORCE_INLINE void LZ4_putPositionOnHash(const BYTE* p, U32 h,
void* tableBase, tableType_t const tableType,
const BYTE* srcBase)
{
switch (tableType)
{
case clearedTable: { /* illegal! */ assert(0); return; }
# 直接存储指针
case byPtr: { const BYTE** hashTable = (const BYTE**)tableBase; hashTable[h] = p; return; }
# 存储在数组中的索引
case byU32: { U32* hashTable = (U32*) tableBase; hashTable[h] = (U32)(p-srcBase); return; }
case byU16: { U16* hashTable = (U16*) tableBase; hashTable[h] = (U16)(p-srcBase); return; }
}
}
算法流程
- 计算当前4个字节的hash值,然后查询hashtable中的value
- 根据value得到字节的位置
- 对比当前4个字节和通过value转换得到的4个字节的值(因为有hash碰撞,所以要校验一下值)
- 如果两者相等,则继续对比后面的字节,找出最长的匹配。然后根据LZ4的格式,写入三元组(未匹配的字符串,匹配字符串的长度,匹配的字符串的位置)
- 如果两者不相等,这向后移动一个字节,继续进行匹配。当前的字节归为未匹配的字符,并将当前的4个字节存储到hashtable中。
算法档位
LZ4档位决定了上面截图中的step。档位越高,step越小(大家对着代码证明一下)。
当前位置未找到重复字符串时,会向后移动step个字节,再次进行匹配。
如果step越大,跳过的字节数越多,进行查找的次数就越少,速度越快。
压缩率会下降,因为被跳过的字节被归为未匹配的字节,但实际上它是有可能找到匹配的。