算术编码的隐形坑:你的压缩为啥总丢比特?
算术编码的隐形坑:你的压缩为什么总在浪费比特?
搞过算术编码的同学,肯定觉得自己挺牛。算法优雅,把比特序列映射到概率区间,一气呵成。可问题是,网上那些基础实现,基本都在悄悄拉胯。
不是说速度慢(虽然CPU周期也重要)。重点是压缩比——你用entropy coding不就为这个吗?
课本代码的猫腻
你肯定见过这种入门代码:
let mut left: u32 = 0;
let mut right: u32 = u32::MAX;
fn encode_bit(bit: bool, probability: f32) {
let mid = left + ((right - left) as f32 * probability) as u32;
if !bit {
right = mid;
} else {
left = mid + 1;
}
while left >> 24 == right >> 24 {
output_byte((left >> 24) as u8);
left <<= 8;
right = (right << 8) | 0xff;
}
}
看着顺眼吧?维护[left, right]区间,每来个比特就按概率缩区间。顶字节确定了,就吐出去,腾空间。
问题是,这代码有隐藏不对称,和数学理想版不符。
字节边界捣乱
怪事来了。理想算术编码里,同等长度的区间一视同仁。但32位整数实现里,不行。
举例:
- left从0起步,区间最小也就2^24比特
- left从2^31-1起步,能缩到2比特那么小
为啥?看输出条件:left >> 24 == right >> 24。这玩意儿看位置,不光看长度。
区间卡在字节边上,比如[2^31-1, 2^31],就容易变窄。概率0.95的比特,硬被量化成50/50。因为区间太短,细粒度没了。
结果?多吐比特,违背entropy理论。
解码端的暗伤
解码也有坑:
fn decode_bit(probability: f32) -> bool {
let mid = left + ((right - left) as f32 * probability) as u32;
if x <= mid {
right = mid;
bit = false;
} else {
left = mid + 1;
bit = true;
}
while left >> 24 == right >> 24 {
left <<= 8;
right = (right << 8) | 0xff;
x = (x << 8) | (bytes.next().unwrap() as u32);
}
bit
}
解码追三个变量:left、right和x(当前码值)。数学上,只需两样:区间长度(right - left)和点位(x - left)。
为啥三个?就为那while循环提精度。必须知道left/right绝对位置,才能查顶字节。
这耦合带来麻烦:
- 寄存器压力大
- 编译器优化难
- 代码难琢磨
明明只该看区间大小,非得管位置。
怎么破?
改思路:别按顶字节相同吐(依赖偏移),改成纯看区间长度(无关偏移)。
听着简单,得重调整个编码解码循环。回报?压缩不浪费比特,解码更简更快。
为啥NameOcean在意?
我们在NameOcean帮开发者搞AI应用,用Vibe Hosting;也优化高性能系统。不管云存储压数据、API payload瘦身,还是大数据智能处理,这些压缩基础都关键。
很多人抄现成库,不知优缺点。压缩比升5-10%,小事?PB级操作或百万API请求时,雪球就大了。
大道理:课本代码不一定完美。多挖挖,懂假设,找不对称。牛工程师不只用算法,还懂边界。
云上性能应用,基础设施抉择会滚雪球。小算法优化,乘以百万请求,效果爆表。想调栈?细节决定成败。