Ukryte pułapki arytmetycznego kodowania: dlaczego tracisz bity w kompresji?

Ukryte pułapki arytmetycznego kodowania: dlaczego tracisz bity w kompresji?

Maj 04, 2026 compression arithmetic-coding entropy-coding algorithms performance-optimization cloud-computing developer-experience

Ukryte pułapki arytmetycznego kodowania: Dlaczego twój kompresor marnuje bity

Jeśli kiedyś wdrażałeś arytmetyczne kodowanie, na pewno czułeś satysfakcję. To sprytny algorytm, który mapuje bity na zakresy prawdopodobieństwa z elegancją. Ale uwaga: większość prostych wersji z sieci działa gorzej, niż myślisz. I to bezszelestnie.

Nie chodzi o prędkość na CPU – choć to też ważne. Kluczowa jest efektywność kompresji. To po nią sięgasz po kodowanie entropijne.

Typowa implementacja, która zawodzi

Pewnie widziałeś taki kod w tutorialach:

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;
    }
}

Proste i logiczne. Śledzisz interwał [left, right]. Każdy bit go zwęża wg prawdopodobieństwa. Gdy bajt jest pewny, wypuszczasz go i zyskujesz precyzję.

Problem? Ta wersja wprowadza asymetrię, której w teorii nie ma.

Zdrada granic bajtów

W idealnym, nieskończenie precyzyjnym kodowaniu interwały tej samej długości są równe. Ale w 32-bitowej wersji? Nie.

Spójrz na przykłady:

  1. Interwał od left = 0 nie zejdzie poniżej 2^24 bitów.
  2. Ten blisko left = 2^31 - 1 skurczy się do 2 bitów.

Dlaczego? Warunek while left >> 24 == right >> 24 zależy od pozycji, nie tylko szerokości.

Gdy interwał łapie granicę bajtu, np. [2^31 - 1, 2^31], staje się mikroskopijny. Prawdopodobieństwa 0.95 kwantyzują się jak 50/50. Efekt? Kompresja emituje za dużo bitów ponad entropię.

Dekoder skrywa własne problemy

W dekoderze jest podobnie:

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
}

Dekoder żongluje left, right i x. Matematycznie wystarczy długość interwału i pozycja punktu.

Pętla precyzyjna wymaga pełnych left i right, by sprawdzić bajty. To spaja logikę z pozycją absolutną, nie względną.

Konsekwencje:

  • Więcej rejestrów.
  • Słabsze optymalizacje kompilatora.
  • Kod trudniejszy do ogarnięcia.

A wszystko przez warunek, który nie powinien patrzeć na adres interwału.

Jak to naprawić

Rozwiązanie? Zmień mechanizm precyzji. Zamiast wyrzucać bajty wg identycznych bitów górnych (zależne od offsetu), rób to po szerokości interwału (niezależnie).

Brzmi łatwo, ale trzeba przebudować pętle kodera i dekodera. Nagroda? Lepsza kompresja i prostszy, szybszy dekoder.

Dlaczego to ważne w NameOcean

W NameOcean pomagamy deweloperom budować appki AI na Vibe Hosting czy zoptymalizowane systemy. Kompresja liczy się w chmurze, API czy przetwarzaniu petabajtów danych.

Wielu bierze biblioteki kompresji bez wglądu w ich limity. 5-10% lepszej kompresji to mało? Spróbuj na milionach requestów.

Lekcja? "Podręcznikowe" kody mają haczyki. Kop głębiej. Szukaj założeń i asymetrii. Najlepsi inżynierowie nie kopiują algorytmów – rozkładają je na części.


Budujesz krytyczne appki w chmurze? Decyzje o infrastrukturze sumują się szybko. Małe poprawki w algo, razy miliony requestów, dają fortunę. Optymalizujesz stack? Szczegóły decydują.

Read in other languages:

RU BG EL CS UZ TR SV FI RO PT NB NL HU IT FR ES DE DA ZH-HANS EN