09 关于 lzf 压缩

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了09 关于 lzf 压缩相关的知识,希望对你有一定的参考价值。

前言

在拷贝前面的这一系列的数据结构的代码的时候, 偶然看到了一个 lzf, 呵呵 稍微看了看 

本文的 lzf 相关代码 拷贝自 redis-6.2.0  

代码来自于 https://redis.io/  

一个简单并且快速的字符压缩算法 

比较常规的通过引用计算来压缩字符串的算法 

关于 lzf 这套代码, redis 项目中也有, liblzf 的官方文档 上面的下载也有 

测试用例

//
// Created by Jerry.X.He on 2021/2/25.
//

#include <iostream>
#include "../libs/sds.h"
#include "../libs/lzf.h"

using namespace std;

int main(int argc, char **argv) {

    sds str = sdsnew("hello hello hello hello ");

    void *compressed = malloc(1024);
    int compressedLen = 1024;
    int realCompressedLen = lzf_compress(str, sdslen(str), compressed, compressedLen);

    void *decompressed = malloc(1024);
    int decompressedLen = 1024;
    int realDecompressedLen = lzf_decompress(compressed, realCompressedLen, decompressed, decompressedLen);

    int x = 0;

}

lzf_decompress 

先来看一下 lzf 的解压, 先看一下 他压缩的大致的结果 和 原始字符串的映射关系, 有助于理解   

# lzf 编码 "hello hello " 之后 
编码之后的结果如下 06 68 65 6c 6c 6f 20 68 20 05 01 6f 20

06 68 65 6c 6c 6f 20 68
# ctrl = 0x06, (ctrl < 32) ctrl + 1 表示是 ref 文本的长度, "hello h"

20 05
# 0x20 = 0b0010 0000 -> len 为 1 + 2 = 3, refOffset 的高5bit 为 0
# 0x05 表示的是 refOffset 的低8bit, ref 为 op - 0x05 - 1
# 这里这个 ref 会复制三个字符串 "ell"

01 6f 20
# ctrl = 0x02, (ctrl < 32), ctrl + 1 表示文本的额长度, "o "

多个 entry, 第一个字符为控制字符, 一个 entry 可能是一个常量, 或者是一个引用 

如果是常量, (ctrl < 32), ctrl + 1[长度为0没有意义]表示的是字符串常量的长度, 紧接着 ctrl+1 个字符, 表示的是常量的数据 

    将该常量拷贝到 结果序列里面 

如果是引用, (ctrl >= 32), 高3bit表示的是长度 len, 如果为7, ctrl后面的一个字节组合表示长度 

    ctrl 的低5bit表示的是 ref 的偏移的高5bit, ctrl 后面的第一字节[len < 7]或者第二字节[len>=7], 表示的是 偏移的低8bit 

    实际引用的长度为 len+2[编码的时候最小2字节相同才处理为引用], 实际的偏移为 ( ((ctrl & 0x1f) << 8) | ctrl[n] )

    然后根据偏移获取 引用的位置, 根据长度拷贝数据信息到 结果序列里面 

最终返回 结果序列的长度 

lzf_compress

先来看一下 lzf 的压缩, 以一个具体的实例来看压缩的过程, 有助于理解   

lzf 压缩 "hello hello ", 最小的共同部分为 长度为2的字符串
"hello hello " 计算 hash, 存放于 htab, 存放 "h" 到 out_data
"ello hello " 计算 hash, 存放于 htab, 存放 "e" 到 out_data
"llo hello " 计算 hash, 存放于 htab, 存放 "l" 到 out_data
"lo hello " 计算 hash, 存放于 htab, 存放 "l" 到 out_data
"o hello " 计算 hash, 存放于 htab, 存放 "o" 到 out_data
" hello " 计算 hash, 存放于 htab, 存放 " " 到 out_data
"hello " 计算 hash, 存放于 htab, 存放 "h" 到 out_data
	这里之所以继续处理为 literal, 是因为代码里面的 "ref > (u8 *) in_data", 个人觉得可以更新为 "ref >= (u8 *) in_data"
"ello " 计算 hash, 获取 htable 里面的元素, 发现其已经存在, 并且在 in_data 里面, 然后进行比较, 获取长度, 放入 (1|0[off高5bit]) = 0b0010 0000, 放入 off 剩余 8bit, 0b0000 0101 放入到 out_data
	呵呵 这里的长度计算似乎是存在问题阿, len 这里最佳期望计算出来应该是 3 才对吧, 这里计算的是 出去末尾2字节, 能匹配的最长的长度 - 2
	按照这里 len 为 1, 会跳过三个字符, 接下来是 "o "
"o ", 这里接下来不超过两个字符了, 直接 literal 编码, 放入 0x01 到 out_data, 放入 "o" 到 out_data, 放入 " " 到 out_data

从第一个字符开始遍历给定的字符串 

根据前三个字节来计算 hash, 将当前 输入指针 的引用放入 hashTable[hash] 里面 

计算当前 指针 和已经存放的 hashTable[hash] 的偏移, 是否在当前字符串的返回内[估算判断], 然后比较数据 至少需要确保 ref 的前两字节 和 当前输入指针的前两字节相同 

    更新之前的常量的长度, 计算当前 ref 的长度, 偏移, 编码当前 ref 的信息[len + offset] 

编码最后的几个字节的数据信息 

返回编码的数据的长度 

lzf 的一些思考 

## lzf 编码 "hello hello " 理想情况下压缩之后的结果为 "05 68 65 6c 6c 6f 20 80 05"
# 长度为 6 的 literal "hello "
05 68 65 6c 6c 6f 20
# 0b 1000 0000, len 为 4, refOffset 高5bit为 0b00000, refOffset 低8bit为 0b0000 0101, refOffset 为 5
# ref 指向 "hello " 真实长度为 len + 2 = 6
80 05


## lzf 编码 "hello hello hello hello " 理想情况下压缩之后的结果为 "05 68 65 6c 6c 6f 20 e0 09 05"
# 长度为 6 的 literal "hello "
05 68 65 6c 6c 6f 20
# 0b 1110 0000, len 标记 为 7, refOffset 高5bit为 0b00000, 接下来 0x09 还是表示长度, len 为 0x07+0x09=16, refOffset 低8bit为 0b0000 0101, refOffset 为 5
# ref 指向 "hello hello hello hello " 真实长度为 len + 2 = 18
e0 09 05

但是为什么 压缩出来的数据并不是我们只管感受能够看到的 "理想的结果" 呢 

我找了一下 官方文档, 似乎是 也没有细节的描述, 可能 得找找相关的论文看看吧 

官方如此设计 应该是针对一些特殊的场景吧, 呵呵 可能是一些 我们目前没有考虑到的场景 

参考

liblzf 的官方文档
liblzf 的 release 库下载

以上是关于09 关于 lzf 压缩的主要内容,如果未能解决你的问题,请参考以下文章

PHP的LZF压缩扩展工具

Apple的LZF算法解析

Apple的LZF算法解析

如何压缩从科特林画廊挑选的照片

Redis持久化方式

PHP的zip压缩工具扩展包学习