#brotli #decompression #huffman #lz77 #no-std

no-std bin+lib divans

DivANS 是一种新的压缩程序结构方式,通过将压缩分解成多个独立可改进的阶段,使它们在更广泛的社区中更容易进行创新。

1 个不稳定版本

使用旧的 Rust 2015

0.0.1 2018年6月19日

#11#lz77

Apache-2.0

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避免序列化这些字节。
    • 在所有必要的字节都被序列化并且环形缓冲区被填充后,最后_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