Az aritmetikus kódolás buktatói: miért maradnak bitek a táblán a kompressziódnál?
Az aritmetikus kódolás buktatói: Miért pazarolod a biteket a kompresszoroddal?
Ha valaha is megírtad az aritmetikus kódolást, tuti büszke voltál magadra. Ez a módszer szépen alakítja a biteket valószínűségi tartományokká. Csakhogy a neten keringő egyszerű verziók csendben alulteljesítenek.
Nem a processzor sebességéről beszélek – bár az is fontos. Hanem a kompressziós arányról, ami miatt egyáltalán entropy kódolást használsz.
A tankönyvi kód, ami mégsem tökéletes
Biztos láttál ilyet a tutorialokban:
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;
}
}
Logikus, ugye? Egy [left, right] intervallumot tartasz karban, ami minden bittel szűkül a valószínűség szerint. Ha egy bájt biztosra vehető, kiírod, és felszabadul a hely a következő biteknek.
A gond? Rejtett aszimmetria van benne, ami a matematikai ideálban nincs.
A bájt-határ csapdája
A tökéletes, végtelen pontosságú verzióban minden azonos hosszúságú intervallum egyformán viselkedik. De a 32 bites integeres kódunkban nem.
Nézzünk két esetet:
- Ha
left = 0, az intervallum sosem zsugorodik 2^24 bit alá. - Ha
left = 2^31 - 1, akár 2 bitig is menhet.
Miért? A left >> 24 == right >> 24 feltétel a pozíciótól függ, nem csak a hossztól.
Ha az intervallum bájt-határon átnyúlik – mondjuk [2^31 - 1, 2^31] –, akkor nagyon kicsi lesz. Ilyenkor a valószínűségek durván kvantálódnak. Egy 0.95-ös bit is 50/50-ra kényszerülhet, mert nincs elég felbontás.
Ennek nyomán több bitet írsz ki, mint amennyit az entrópiaelmélet szerint kellene.
A dekódoló rejtett bonyolultsága
A dekódolóban is van baj:
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
}
Három változót követ: left, right és x (a bemeneti érték). Pedig matematikailag elég kettő: az intervallum hossza (right - left) és a pont helyzete (x - left).
A ciklus miatt kell mindhárom. A dekódoló a pozíciótól függően kezeli a pontosságot, nem a méret alapján.
Ez azt hozza:
- Több regiszterhasználat
- Kevesebb optimalizálási lehetőség
- Nehezebb kód megértése
Mindez egy olyan feltétel miatt, ami csak a méretet kéne néznie.
Hogyan javítsd meg?
Állítsd át a pontosságot: ne pozíciófüggő bájtkidobást használj, hanem méretfüggőt.
Nem triviális, az egész kódot át kell nézni. De cserébe nem veszítesz biteket, és a dekódoló gyorsabb, egyszerűbb lesz.
Miért érdekes ez a NameOcean-nél?
A NameOcean-nél fejlesztőkkel dolgozunk, akik AI appokat építenek Vibe Hostinggal vagy optimalizált rendszereket. Adatokat tömörítesz felhőbe, API-t optimalizálsz, vagy nagy adathalmazokat darálasz? Érdemes ismerni ezeket.
Sokan kész libeket vesznek, anélkül hogy tudnák a határaikat. 5-10% jobb arány apróságnak tűnik, de petabájtos műveleteknél vagy milliók API-kéréseinél hatalmas.
A tanulság: a "tankönyvi" kódok nem mindig ideálisak. Nézd meg alaposan. Kérdőjelezd meg a furcsaságokat. A jó mérnökök nem csak használnak algoritmust – értik a széleket.
Ha teljesítménykritikus cloud appot építesz, Infrastructure-döntéseid összeadódnak. Kisebb algo-javítások milliók alatt óriási előny. Optimalizálni akarsz? A részletek dölik el.