Los errores ocultos del arithmetic coding: por qué tu compresión deja bits en la mesa
Los Errores Ocultos del Arithmetic Coding: Por Qué Tu Compresor Pierde Bits Sin que lo Notes
Implementar arithmetic coding te hace sentir un genio. Es un algoritmo limpio que convierte secuencias de bits en rangos probabilísticos precisos. Pero la realidad es dura: la mayoría de códigos básicos que circulan por la web están fallando en silencio.
No hablo de velocidad en CPU (aunque eso cuenta). Me refiero al ratio de compresión, el motivo real para usar codificación entrópica.
La Versión "Clásica" que Engaña
Seguro has visto ejemplos así en tutoriales:
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;
}
}
Parece lógico. Mantienes un intervalo [left, right] que se achica con cada bit según su probabilidad. Cuando un byte se estabiliza, lo emites y liberas precisión.
El fallo: hay una asimetría sutil que el modelo matemático ideal no tiene.
El Problema de las Fronteras de Byte
En la teoría perfecta, todos los intervalos del mismo tamaño actúan igual. Con enteros de 32 bits, no.
Imagina dos casos:
- Un intervalo en
left = 0no baja de 2^24 bits. - Uno en
left = 2^31 - 1puede reducirse a solo 2 bits.
Culpa de la condición while left >> 24 == right >> 24. Depende de la posición del intervalo, no solo de su ancho.
Si el intervalo cruza una frontera de byte, como [2^31 - 1, 2^31], se hace minúsculo. Las probabilidades se redondean burdamente. Un bit con p=0.95 termina como un 50/50 porque no hay precisión para más.
Resultado: emites bits de más, contra lo que dicta la entropía.
El Decodificador y Su Trampa Interna
El decodificador tiene su propio lío:
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
}
Sigue left, right y x (el valor codificado). Matemáticamente bastan dos: longitud del intervalo (right - left) y posición relativa (x - left).
¿Por qué tres? Por el bucle de precisión, atado a la posición absoluta. Esto genera:
- Más presión en registros.
- Menos optimizaciones del compilador.
- Código confuso de razonar.
Todo por una condición que solo debería mirar el tamaño, no la ubicación.
Cómo Solucionarlo
Cambia el enfoque de precisión. En vez de emitir bytes por bytes idénticos en la cima (depende de offset), hazlo por longitud del intervalo (independiente de offset).
Suena fácil, pero recalibra todo el bucle de encode/decode. Ganancia: compresión óptima y decodificador más simple y rápido.
Por Qué Importa en NameOcean
En NameOcean ayudamos a devs que crean apps con AI en Vibe Hosting o sistemas ultraoptimizados. Comprimir datos para storage en cloud, payloads de API o datasets masivos es clave.
Muchos usan librerías sin conocer sus límites. Un 5-10% mejor en ratio parece poco... hasta que manejas petabytes o millones de requests.
La lección: los ejemplos "estándar" esconden trampas. Analiza supuestos. Busca asimetrías. Los mejores ingenieros no copian algoritmos; los dominan en sus casos extremos.
Si armas apps críticas en cloud, tus choices en infra se multiplican. Pequeños fixes algorítmicos, a escala masiva, suman enorme. ¿Quieres tunear tu stack? Los detalles mandan.