在做 IoT 设备遥测、监控指标收集、时序数据库或高频交易日志存储时,浮点数往往是吞吐量与存储体积的主要瓶颈。
真实系统里采集到的浮点数据,绝大多数并非全精度的无规律随机数,而是带有固定小数位读数的十进制数(例如传感器温度 24.50、电压 3.31、价格 199.99)。
但是按 IEEE 754 规范存储时,尾数与指数被拆散分布在 64 位或 32 位中。直接使用常规的通用字节压缩器(LZ4、Snappy、Zstd),由于局部字节重复度低,压缩率通常很平庸(只有 3x 左右),且吞吐会被限制在几个 GB/s 内。以异或为基础的浮点压缩(如 Gorilla)虽然速度较快,但遇到跳变或跨量级数据时压缩率也很受限。
SIGMOD 2024 上 CWI 数据库团队提出了 ALP(Adaptive Lossless Floating-Point Compression)算法,核心思想是将十进制浮点数自适应投影到整数域,再结合基准参考系(Frame-of-Reference, FOR)进行紧凑位打包。
但在研读官方 C++ 参考实现并在实际工程中落地时,我发现几个明显的瓶颈:
- 采样开销过重:原版在采样阶段进行无剪枝的全局穷举,采样占用了超过 80% 的 CPU 周期,导致压缩吞吐被拖累在 0.8 GB/s 左右。
- 解压内存往返:在 Delta 模式下采用两遍式解码,需要先将差分解包到一个 8KB 的栈缓冲区,再做前缀和与浮点转换,带来了额外的缓存开销。
- 乘法截断误差:二进制浮点乘法(如乘以 0.1)受 IEEE 754 精度影响,容易出现可逆性校验失败,导致很多本该正常打包的浮点数被丢进异常字典,恶化了压缩率。
- 舍入边界溢出:依赖浮点 Magic Constant 舍入,在超出 2^51 范围时会产生边界溢出。
为了在 Rust 生态中获得更高的编解码吞吐和更小的体积,我从底层微架构和执行流水线出发,重构了这一算法并开源了 fastalp。
核心机制与优化
fastalp 是纯 Rust 实现,零第三方依赖,支持 no_std。针对上述问题做了针对性的算法与工程优化:
-
三级级联剪枝流水线: 在编码采样阶段引入三级快速筛选:单周期判断全等序列、4/16 样本短路跳出不规则离群点、以及非十进制特征提前中止。跳过了 70% 到 90% 的无效搜索空间,将端到端压缩吞吐提升至 2.1 GB/s(较原版提速约 2.6 倍)。
-
融合单遍 AlpConsumer 寄存器解码流水线: 传统解码需要分配中间缓冲区。fastalp 将位解包、差分前缀和累加、FOR 基准偏移计算与 IEEE 754 浮点重构全部融合在寄存器内以单遍完成。消除了 8KB 栈缓冲区的读写开销,几何平均解压吞吐达到 25.3 GB/s。
-
精确十进制除法重构: 针对浮点乘法精度截断导致的虚假异常,增加了除法路径的校验重构,彻底收窄了异常字典大小,数据体积进一步缩小。
-
硬件原生银行家舍入: 直接映射到现代 CPU 原生硬件舍入指令(ARM64 上的 FRINTN、x86 上的 ROUNDSD),移除了 2^51 动态范围限制,且消除了分支预测失败开销。
-
原生支持 f32 与 f64: 提供统一的泛型接口,同时覆盖单精度与双精度浮点流。
性能基准对比
在 Apple M2 Max 芯片上,针对官方 ALP 基准测试集的全部 37 个公开和工业数据集(涵盖电力监测、气象遥测、高频量化、城市传感器等)进行全量评估,跨所有数据集的几何平均值(GeoMean)如下:
| 编解码器 | 类别 | 解压吞吐量 | 端到端压缩吞吐 | 几何平均压缩比 |
|---|---|---|---|---|
| fastalp (Rust) | 专用浮点压缩 | 25.3 GB/s | 2.1 GB/s | 9.50x |
| C++ ALP (论文原版) | 专用浮点压缩 | 19.3 GB/s | 0.8 GB/s | 5.93x |
| Pcodec (pco) | 专用浮点压缩 | 1.8 GB/s | 0.2 GB/s | 8.81x |
| LZ4 (lz4_flex) | 通用字节压缩 | 5.0 GB/s | 2.0 GB/s | 3.89x |
| Snappy (snap) | 通用字节压缩 | 4.6 GB/s | 2.5 GB/s | 3.05x |
| Zstd (level 3) | 通用字节压缩 | 1.4 GB/s | 0.5 GB/s | 6.07x |
| Gorilla | 专用浮点压缩 | 1.2 GB/s | 1.9 GB/s | 4.41x |
在城市指标等结构规整的数据上,解码吞吐可达 40 到 65 GB/s;对于连续平稳的波形数据,配合一阶差分,压缩比可达数十倍以上。
安装与使用
在 Cargo.toml 中引入:
[dependencies]
fastalp = "0.1"
基础压缩与解压:
use fastalp::{compress, decompress, Result};
fn main() -> Result<()> {
let sensor_data = vec![20.5, 20.6, 20.8, 21.0, 20.9, 21.2];
// 压缩为字节数组(泛型支持 f64 / f32)
let compressed = compress(&sensor_data);
// 无损还原原始数据
let decompressed: Vec<f64> = decompress(&compressed)?;
assert_eq!(decompressed, sensor_data);
Ok(())
}
在高频流水线中,可以使用就地复用缓冲区消除内存分配开销:
use fastalp::{compress_into, decompress_into, Result};
fn main() -> Result<()> {
let data = vec![1.23, 1.24, 1.25, 1.26];
let mut out_buf = Vec::new();
// 复用输出缓冲区
compress_into(&data, &mut out_buf);
let mut restored = vec![0.0; data.len()];
decompress_into(&out_buf, &mut restored)?;
assert_eq!(restored, data);
Ok(())
}
适用场景与局限
明确一下 fastalp 的适用边界:
适合场景:
- 物联网传感器读数、GPS 遥测经纬度、时序监控指标。
- 固定精度浮点数、阶梯波形或局部平稳波形。
- 对解压延迟敏感的实时时序存储引擎或内存缓存。
局限与非适用场景:
- 如果输入是完全均匀随机的高熵浮点数(例如蒙特卡洛模拟生成的随机数或伪随机噪声),十进制缩放模型无法命中规律,算法会自动回退到存储原始 IEEE 754 位流,压缩率会接近 1x。
- 单次传入数据量建议在 1000 个元素以上(默认以 1024 元素为向量化分块单位),极小样本块无法分摊元数据开销。
代码已在 GitHub 开源,并在 crates.io 发布。欢迎在各自的时序场景下体验压测。如果遇到特殊分布的边界 case 或建议,欢迎在 GitHub 提 issue 交流。