1 个不稳定版本
使用旧的 Rust 2015
0.0.1 | 2018年6月19日 |
---|
#11 在 #lz77
2MB
27K SLoC
% divANS 模块
概述
divANS 库旨在用于通用数据压缩。该算法经过调整,在压缩比和性能之间取得了显著的平衡,在 150 Mbit/s 的行速度下运行。
该名称来源于 "divided-ANS",因为中间表示形式是从 ANS 编码器中分离出来的
更多信息请参阅 https://blogs.dropbox.com/tech/2018/06/building-better-compression-together-with-divans/
Divans 应主要考虑用于冷存储和压缩研究。该压缩算法高度模块化,新的算法只需编写一次,因为通用的特化结构在编译时为压缩和解压缩优化了编解码器。
Rust 使用
解压缩
extern crate divans;
fn main() {
use std::io;
let stdin = &mut io::stdin();
{
use std::io::{Read, Write};
let mut reader = divans::DivansDecompressorReader::new(
stdin,
4096, // buffer size
);
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf[..]) {
Err(e) => {
if let io::ErrorKind::Interrupted = e.kind() {
continue;
}
panic!(e);
}
Ok(size) => {
if size == 0 {
break;
}
match io::stdout().write_all(&buf[..size]) {
Err(e) => panic!(e),
Ok(_) => {},
}
}
}
}
}
}
压缩
extern crate divans;
fn main() {
use std::io;
let stdout = &mut io::stdout();
{
use std::io::{Read, Write};
let mut writer = divans::DivansBrotliHybridCompressorWriter::new(
stdout,
divans::DivansCompressorOptions{
literal_adaptation:None, // should we override how fast the cdfs converge for literals?
window_size:Some(22), // log 2 of the window size
lgblock:None, // should we override how often metablocks are created in brotli
quality:Some(11), // the quality of brotli commands
dynamic_context_mixing:Some(2), // if we want to mix together the stride prediction and the context map
use_brotli:divans::BrotliCompressionSetting::default(), // ignored
use_context_map:true, // whether we should use the brotli context map in addition to the last 8 bits of each byte as a prior
force_stride_value: divans::StrideSelection::UseBrotliRec, // if we should use brotli to decide on the stride
},
4096, // internal buffer size
);
let mut buf = [0u8; 4096];
loop {
match io::stdin().read(&mut buf[..]) {
Err(e) => {
if let io::ErrorKind::Interrupted = e.kind() {
continue;
}
panic!(e);
}
Ok(size) => {
if size == 0 {
match writer.flush() {
Err(e) => {
if let io::ErrorKind::Interrupted = e.kind() {
continue;
}
panic!(e)
}
Ok(_) => break,
}
}
match writer.write_all(&buf[..size]) {
Err(e) => panic!(e),
Ok(_) => {},
}
}
}
}
}
}
C 语言使用
C api 是一个标准的压缩 API,类似于 zlib 提供的 API。尽管是 Rust 代码,但除非传递了 CAllocator 结构并设置 custom_malloc 字段为 NULL,否则不会进行分配。这意味着任何 divans 库的使用者都可以提供自己的分配系统,所有分配都将通过该分配系统进行。custom_malloc 返回的指针必须是 32 字节对齐的。
压缩
#include "divans/ffi.h"
// compress to stdout
DivansResult compress(const unsigned char *data, size_t len) {
unsigned char buf[4096];
struct CAllocator alloc = {custom_malloc, custom_free, custom_alloc_opaque}; // set all 3 to NULL to use rust allocators
struct DivansCompressorState *state = divans_new_compressor_with_custom_alloc(alloc);
divans_set_option(state, DIVANS_OPTION_USE_CONTEXT_MAP, 1);
divans_set_option(state, DIVANS_OPTION_DYNAMIC_CONTEXT_MIXING, 2);
divans_set_option(state, DIVANS_OPTION_QUALITY, 11);
while (len) {
size_t read_offset = 0;
size_t buf_offset = 0;
DivansResult res = divans_encode(state,
data, len, &read_offset,
buf, sizeof(buf), &buf_offset);
if (res == DIVANS_FAILURE) {
divans_free_compressor(state);
return res;
}
data += read_offset;
len -= read_offset;
fwrite(buf, buf_offset, 1, stdout);
}
DivansResult res;
do {
size_t buf_offset = 0;
res = divans_encode_flush(state,
buf, sizeof(buf), &buf_offset);
if (res == DIVANS_FAILURE) {
divans_free_compressor(state);
return res;
}
fwrite(buf, buf_offset, 1, stdout);
} while(res != DIVANS_SUCCESS);
divans_free_compressor(state);
return DIVANS_SUCCESS;
}
解压缩
#include "divans/ffi.h"
//decompress to stdout
DivansResult decompress(const unsigned char *data, size_t len) {
unsigned char buf[4096];
struct CAllocator alloc = {custom_malloc, custom_free, custom_alloc_opaque}; // set all 3 to NULL for using rust allocators
struct DivansDecompressorState *state = divans_new_decompressor_with_custom_alloc(alloc);
DivansResult res;
do {
size_t read_offset = 0;
size_t buf_offset = 0;
res = divans_decode(state,
data, len, &read_offset,
buf, sizeof(buf), &buf_offset);
if (res == DIVANS_FAILURE || (res == DIVANS_NEEDS_MORE_INPUT && len == 0)) {
divans_free_decompressor(state);
return res;
}
data += read_offset;
len -= read_offset;
fwrite(buf, buf_offset, 1, stdout);
} while (res != DIVANS_SUCCESS);
divans_free_decompressor(state);
return DIVANS_SUCCESS;
}
divANS 代码库结构
顶级模块
模块 | 目的 |
---|---|
probability | 16 位 4 位 CDF 的优化实现,支持在线训练和归一化 |
codec/interface | CrossCommandState 跟踪 brotli 命令之间需要保持的数据。例如,CDF、前几个字节、复制用环形缓冲区等 |
codec/dict | 对文件中可能由包含的 brotli 字典产生的部分进行编码/解码 |
codec/copy | 对文件中之前已见过且仍在环形缓冲区中的部分进行编码/解码 |
codec/block_type | 对文件中的标记进行编码/解码,divans 可以将其用作字面量、距离或甚至命令类型的先验 |
codec/context_map | 对 brotli context_map 进行编码/解码,该 map 将前 6 位和 literal_block_type 重映射到 0 到 255 之间的先验 |
codec/literal | 对文件中出现的原始数据进行编码/解码。这可以使用多种策略或策略组合来编码每个半字节 |
codec/priors | 定义包含关于过去数据的统计信息的动态训练 CDF 表大小的结构体 |
codec/weights | 基于先验有效性的多个 CDF 混合的结构体 |
编解码器/特殊化 | 优化系统,用于为当前运行的 nibble 解码或编码路径生成单独的代码路径,基于选定的先验 |
编解码器 | 编码/解码整体命令本身,并跟踪整体文件压缩的状态以及是否完成 |
divans_decompressor | 解压器特质的实现,解析 divans 标头并将 ANS 流转换为命令和原始数据 |
brotli_ir_gen | 压缩器特质的实现,调用 brotli 编码器并提取每个要编码的元块的命令数组 |
divans_compressor | 压缩器特质的另一种实现,调用 raw_to_cmd 而不是 brotli 来获取每个元块的命令数组 |
divans_to_raw | 编解码器专用解码器,假设默认输入命令并逐步填充它们 |
cmd_to_divans | 编解码器专用编码器,接受输入命令并生成 divans |
raw_to_cmd | 未来:Brotli 压缩器的替代品,用于生成命令 |
cmd_to_raw | 解释 Brotli 命令列表并生成未压缩文件 |
arithmetic_coder | 定义算术编码器和算术解码器算术编码器特质 |
ans | EntropyEncoder 和 EntropyDecoder 接口的快速实现 |
billing | 插件,通过提供相同的接口并包装编码器/解码器来添加对算术编码器/解码器的归属 |
alloc_util | 分配器,在许多分配中重复使用单个内存切片 |
slice_util | 一种借用和引用现有切片的机制,当 divans 返回给调用者请求更多输入或输出空间时,可以冻结切片,取消借用切片 |
resizable_buffer | 简单的可调整大小字节缓冲区,可以存储正在处理的原始输入和输出流 |
reader | divans 编码和解码的读取实现 |
writer | divans 编码和解码的写入实现 |
总体流程
要编码文件,
- 一个
writer::DivansBrotliHybridCompressorWriter
实例化一个brotli_ir_gen::BrotliDivansHybridCompressor
- 该压缩器既有来自 brotli crate 的
brotli::BrotliEncoderStateStruct
,也有一个codec::DivansCodec<ANSEncoder, EncodeSpecialization>
. - 使用
brotli_ir_gen::BrotliDivansHybridCompressor::encode
,压缩器通过调用brotli::BrotliEncoderCompressStream
将输入数据馈入brotli::BrotliEncoderStateStruct
- 通过调用
brotli::BrotliEncoderCompressStream
- 通过调用
brotli::BrotliEncoderCompressStream
可以触发调用brotli_ir_gen::BrotliDivansHybridCompressor::divans_encode_commands
- 回调将包括一个
brotli::interface::Command
项的切片 - 这些项被馈入
codec::DivansCodec<ANSEncoder, EncodeSpecialization>::encode_or_decode
,将其编码为 divans 格式。codec::DivansCodec<ANSEncoder, cmd_to_divans::EncoderSpecialization>::encode_or_decode
通过使用EncoderSpecialization
来拉取输入命令作为真实来源来完成这项任务。- 不幸的是,brotli可以将尽可能多的数据传递给调用者,最多可达16兆的最大元块大小。
- 这意味着调用者必须在一个
resizable_buffer::ResizableBuffer
中缓冲这些数据。
- 这意味着调用者必须在一个
- 回调将包括一个
- 当所有回调完成时,
brotli_ir_gen::BrotliDivansHybridCompressor::encode_stream
会尽可能地将原始缓冲区刷新。 - 最终,当用户调用
brotli_ir_gen::BrotliDivansHybridCompressor::flush
时,将遵循类似的程序,但设置了完成标志。
要解码一个文件,
- 实例化一个
reader::DivansDecompressorReader
来创建一个divans_decompressor::DivansDecompressor
。 - 解压器是一个枚举,在解析完16字节的原始头部后,从
HeaderParser
模式切换到Decode
模式。 divans_decompressor::DivansDecompressor::Decode
包含一个codec::DivansCodec<ANSDecoder, DecoderSpecialization>
。- 使用
brotli_ir_gen::DivansDeompressor::decode
,解压器将输入数据直接馈入codec::DivansCodec<ANSDecoder, DecoderSpecialization>
。- 编解码器的
encode_or_decode
方法旨在在编码时接收命令作为输入,因此divans_to_raw::DecoderSpecialization
简单地为每种类型的命令创建占位符命令,以便相同的代码路径可以编码和解码命令。
- 编解码器的
- 当达到最终状态时,写入校验和并返回成功。
编解码器状态机
codec::DivansCodec
有三个成员,cross_command_state
跟踪概率模型,state
跟踪正在解码的命令类型,以及codec_traits
,用作存储编译器常量值的仓库,这些值在解码或编码阶段基于头部和命令数据被设置为这种方式。
state
值是一个枚举,可以携带特定命令的信息,或者可以标记环形缓冲区必须被填充等。
可用编解码器状态的概述
Begin
:此状态表示解码器不在编码特定命令的过程中,因此下一步将是解码下一个命令。Literal(literal::LiteralState
:编码器正在将原始字面量编码为要注入到文件中的过程。Dict(dict::DictState)
:编码器正在编码brotli字典中出现的单词Copy(copy::CopyState)
:编码器正在编码从环形缓冲区拉取数据的引用BlockSwitchLiteral(block_type::LiteralBlockTypeState)
:编码器被指示序列化一个将影响预测器如何模拟未来字面值任意值的值BlockSwitchCommand(block_type::BlockTypeState)
:编码器被指示序列化一个将影响预测器如何模拟无(待办事项)任意值的值BlockSwitchDistance(block_type::BlockTypeState)
:编码器被指示序列化一个将影响预测器如何模拟从和字典值距离的任意值PredictionMode(context_map::PredictionModeState)
:编码器被指示序列化一个上下文映射,将BlockSwitchLiteral值和最后6位映射到[0, 255]范围内的值,用作训练CDF数组的索引PopulateRingBuffer(Command<u8, AllocU8>>)
当字面值、字典或复制状态达到终止状态时,这些状态将被移动到PopulateRingBuffer状态。PopulateRingBuffer
使用存储在DivanCodec::CrossCommandState
中的cmd_to_raw::DivansRecoderState
来填充环形缓冲区- 如果选择DecoderSpecialization,
cmd_to_raw::DivansRecoderState
将数据复制到输出字节,返回并请求NeedBytes,直到所有字节都被序列化 - 否则,编码器Specialization避免序列化这些字节。
- 如果选择DecoderSpecialization,
- 在所有必要的字节都被序列化并且环形缓冲区被填充后,最后_8_字面值被保存以用作未来的先验值
WriteChecksum(usize)
这种状态发生在解码过程中遇到结束命令(0xf)或在编码时遇到code::DivansCodec::flush
- 目前,校验和支持未激活,但8个字节被简单地序列化
DivansSuccess
当解码器上的WriteChecksum完成或编码器上的最终命令到达时,达到此状态EncodedShutdownNode
|ShutdownCoder
|CoderBufferDrain
仅出现在编码器在flush/close之后,EOF节点类型被刷新时
致谢
特别感谢Jaroslaw(Jarek)Duda和Fabian Giesen的杰出工作以及他们对ANS算法的详细和深思熟虑的介绍。
依赖项
~6.5MB
~269K SLoC