Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat

Ha Ön fejlesztő, és a kódolás kiválasztásával kell szembenéznie, akkor szinte mindig az Unicode lesz a megfelelő megoldás. A konkrét ábrázolási mód a kontextustól függ, de leggyakrabban itt is van egy univerzális válasz - UTF-8. Az a jó benne, hogy lehetővé teszi az összes Unicode karakter használatát költés nélkül túl sokat sok bájt a legtöbb esetben. Igaz, azoknál a nyelveknél, amelyek nem csak a latin ábécét használják, a „nem túl sok” az legalább két bájt karakterenként. Tudunk-e jobbat csinálni anélkül, hogy visszatérnénk az őskori kódolásokhoz, amelyek csak 256 elérhető karakterre korlátoznak bennünket?

Az alábbiakban azt javaslom, hogy ismerkedjen meg a kérdés megválaszolására tett kísérletemkel, és egy viszonylag egyszerű algoritmust valósítson meg, amely lehetővé teszi a sorok tárolását a világ legtöbb nyelvén anélkül, hogy hozzáadná az UTF-8 redundanciáját.

Jogi nyilatkozat. Azonnal teszek néhány fontos fenntartást: a leírt megoldást nem kínálják az UTF-8 univerzális helyettesítőjeként, csak az esetek szűk listájában alkalmas (erről bővebben lentebb), és semmi esetre sem szabad harmadik féltől származó API-kkal való interakcióra használni (akik nem is tudnak róla). Leggyakrabban az általános célú tömörítési algoritmusok (például deflate) alkalmasak nagy mennyiségű szöveges adat kompakt tárolására. Ezen túlmenően, már a megoldás létrehozása során találtam egy létező szabványt magában a Unicode-ban, amely ugyanazt a problémát oldja meg - ez valamivel bonyolultabb (és gyakran rosszabb is), de mégis elfogadott szabvány, és nem csak úgy. együtt a térdén. Majd mesélek róla is.

Az Unicode-ról és az UTF-8-ról

Kezdésként néhány szó arról, hogy mi is ez Unicode и UTF-8.

Mint tudják, a 8 bites kódolások korábban népszerűek voltak. Velük minden egyszerű volt: 256 karakter számozható 0-tól 255-ig, a 0-tól 255-ig terjedő számok pedig nyilván egy bájtként ábrázolhatók. Ha visszamegyünk a legelejére, akkor az ASCII kódolás teljesen 7 bitre van korlátozva, így a bájtábrázolásában a legjelentősebb bit nulla, és a legtöbb 8 bites kódolás kompatibilis vele (csak a „felsőben” különböznek egymástól). rész, ahol a legjelentősebb bit egy ).

Miben különbözik a Unicode ezektől a kódolásoktól, és miért van hozzá olyan sok specifikus ábrázolás társítva – UTF-8, UTF-16 (BE és LE), UTF-32? Tegyük sorba.

Az alap Unicode szabvány csak a karakterek (és bizonyos esetekben a karakterek egyes összetevői) és a számok közötti megfelelést írja le. És nagyon sok lehetséges szám van ebben a szabványban - tól 0x00 a 0x10FFFF (1 114 112 darab). Ha egy ilyen tartományban lévő számot szeretnénk egy változóba beletenni, akkor nekünk sem 1, sem 2 bájt nem lenne elég. És mivel processzorainkat nem nagyon tervezték három bájtos számokkal való munkára, kénytelenek lennénk karakterenként akár 4 bájtot is használni! Ez az UTF-32, de éppen ez a „pazarlás” miatt nem népszerű ez a formátum.

Szerencsére a karakterek sorrendje a Unicode-on belül nem véletlen. A teljes készletük 17 hüvelykesrepülőgépek", amelyek mindegyike 65536 (0x10000) «kódpontok" A „kódpont” fogalma itt egyszerű karakterszám, amelyet a Unicode rendelt hozzá. De, mint fentebb említettük, a Unicode-ban nem csak az egyes karakterek vannak számozva, hanem azok összetevői és szolgáltatási jelei is (és néha semmi sem felel meg a számnak - talán egyelőre, de nekünk ez nem annyira fontos), így Helyesebb, ha mindig konkrétan magukról a számokról beszélünk, nem a szimbólumokról. A következőkben azonban a rövidség kedvéért gyakran használom a „szimbólum” szót, ami a „kódpont” kifejezésre utal.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
Unicode repülőgépek. Amint látja, a legtöbb (4-13-as gépek) még mindig kihasználatlan.

Ami a legfigyelemreméltóbb, az az, hogy az összes fő „pép” a nulla síkban fekszik, ezt hívják „Alap többnyelvű sík". Ha egy sor szöveget tartalmaz a modern nyelvek egyikén (beleértve a kínait is), akkor nem lép túl ezen a síkon. De nem vághatja le a Unicode többi részét sem - például az emojik többnyire a nyelv végén találhatók a következő gép"Kiegészítő többnyelvű sík"(tól kezdve terjed ki 0x10000 a 0x1FFFF). Tehát az UTF-16 ezt teszi: az összes karakter, amelyen belül van Alap többnyelvű sík, kódolása „ahogy van” a megfelelő kétbájtos számmal. Azonban néhány szám ebben a tartományban egyáltalán nem jelöl konkrét karaktereket, hanem azt jelzi, hogy e pár bájt után egy másikat is figyelembe kell vennünk - ennek a négy bájtnak az értékét összevonva egy olyan számot kapunk, amely lefedi a teljes érvényes Unicode-tartományt. Ezt az ötletet "pótpároknak" hívják – talán hallott már róluk.

Tehát az UTF-16 két vagy (nagyon ritka esetekben) négy bájtot igényel "kódpontonként". Ez jobb, mintha állandóan négy bájtot használnánk, de a latin (és más ASCII karakterek) így kódolva a hely felét a nullákra pazarolja. Az UTF-8 ennek kijavítására készült: az ASCII benne, mint korábban, csak egy bájtot foglal el; kódok innen 0x80 a 0x7FF - két bájt; tól től 0x800 a 0xFFFF - három, és től 0x10000 a 0x10FFFF - négy. Egyrészt jó lett a latin ábécé: visszatért a kompatibilitás az ASCII-vel, és az eloszlás egyenletesebben „oszlik el” 1-4 byte között. A latinon kívüli ábécék azonban, sajnos, semmi hasznot nem hoznak az UTF-16-hoz képest, és sokuknál ma már három bájtra van szükség kettő helyett – a kétbájtos rekord tartománya 32-szeresére szűkült. 0xFFFF a 0x7FF, és sem kínai, sem például grúz nem szerepel benne. Cirill és öt másik ábécé - hurrá - szerencse, karakterenként 2 bájt.

Miért történik ez? Nézzük meg, hogyan ábrázolja az UTF-8 a karakterkódokat:
Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
Közvetlenül a számok ábrázolására itt a szimbólummal jelölt biteket használjuk x. Látható, hogy egy kétbájtos rekordban csak 11 ilyen bit van (16-ból). A vezető biteknek itt csak segédfunkciójuk van. Egy négybájtos rekord esetén a 21 bitből 32 van lefoglalva a kódpontszám számára - úgy tűnik, hogy három bájt (ami összesen 24 bitet ad) elég lenne, de a szolgáltatásjelzők túl sokat fogyasztanak.

Ez rossz? Nem igazán. Egyrészt, ha nagyon törődünk a térrel, vannak tömörítési algoritmusaink, amelyek könnyedén kiküszöbölik az összes extra entrópiát és redundanciát. Másrészt a Unicode célja az volt, hogy a lehető leguniverzálisabb kódolást biztosítsa. Például rábízhatunk egy UTF-8 kódolású sort arra a kódra, amely korábban csak ASCII-vel működött, és nem kell félni attól, hogy az ASCII tartományból olyan karaktert fog látni, amely valójában nincs ott (végül is az UTF-8-ban minden bájtok a nulla bittől kezdődően – pontosan ez az ASCII). És ha hirtelen le akarunk vágni egy kis farkot egy nagy karakterláncról anélkül, hogy a legelején dekódolnánk (vagy visszaállítanánk az információ egy részét egy sérült szakasz után), akkor könnyen megtaláljuk az eltolást, ahol egy karakter kezdődik (elég a bit előtaggal rendelkező bájtok kihagyásához 10).

Miért kell akkor valami újat kitalálni?

Ugyanakkor időnként vannak olyan helyzetek, amikor az olyan tömörítési algoritmusok, mint a deflate, rosszul alkalmazhatók, de a karakterláncok kompakt tárolását szeretné elérni. Személy szerint én találkoztam ezzel a problémával, amikor az építésről gondolkodtam tömörített előtag fa egy nagy szótárhoz, amely tetszőleges nyelvű szavakat tartalmaz. Egyrészt minden szó nagyon rövid, így a tömörítés hatástalan lesz. Másrészt az általam figyelembe vett fa megvalósítást úgy tervezték meg, hogy a tárolt karakterlánc minden bájtja külön fa csúcsot generált, így számuk minimalizálása nagyon hasznos volt. A könyvtáramban Az.js (Mint a pimorfia2, amelyen alapul) egy hasonló probléma egyszerűen megoldható - karakterláncokba csomagolva Dawg-szótár, ott tárolva jó öreg CP1251. De amint az könnyen érthető, ez csak korlátozott ábécé esetén működik jól – egy kínai nyelvű sort nem lehet hozzáadni egy ilyen szótárhoz.

Külön szeretném megjegyezni még egy kellemetlen árnyalatot, amely az UTF-8 ilyen adatstruktúrában történő használatakor merül fel. A fenti képen látható, hogy ha egy karaktert két bájtként írunk fel, akkor a számához kapcsolódó bitek nem sorban jönnek, hanem egy bitpár választja el őket 10 középen: 110xxxxx 10xxxxxx. Emiatt amikor a második bájt alsó 6 bitje túlcsordul a karakterkódban (azaz átmenet történik 1011111110000000), akkor az első bájt is megváltozik. Kiderült, hogy a „p” betűt bájtok jelölik 0xD0 0xBF, és a következő „r” már az 0xD1 0x80. Az előtagfában ez a szülőcsomópont kettéosztásához vezet – az egyik az előtag számára 0xD0, egy másik pedig 0xD1 (bár a teljes cirill ábécét csak a második bájt tudta kódolni).

mit kaptam

Ezzel a problémával szembesülve úgy döntöttem, hogy gyakorolni fogom a bitekkel való játékot, és ezzel egyidejűleg egy kicsit jobban megismerem a Unicode szerkezetének egészét. Az eredmény az UTF-C kódolási formátum ("C" for kompakt), amely kódpontonként legfeljebb 3 bájtot költ, és nagyon gyakran csak költést tesz lehetővé egy extra bájt a teljes kódolt sorhoz. Ez ahhoz a tényhez vezet, hogy sok nem ASCII ábécé esetén az ilyen kódolás 30-60%-kal kompaktabb, mint az UTF-8.

Példákat mutattam be kódoló és dekódoló algoritmusok megvalósítására a formában JavaScript és Go könyvtárak, szabadon használhatja őket a kódjában. De továbbra is hangsúlyozom, hogy bizonyos értelemben ez a formátum „bicikli” marad, és nem javaslom a használatát anélkül, hogy felfognád, miért van rá szükséged. Ez még mindig inkább kísérlet, mint komoly „UTF-8 továbbfejlesztése”. Ennek ellenére a kód szépen, tömören van megírva, sok megjegyzéssel és tesztlefedettséggel.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
Teszt eredmények és összehasonlítás UTF-8-cal

én is megtettem bemutató oldal, ahol kiértékelheti az algoritmus teljesítményét, majd bővebben mesélek az elveiről és a fejlesztési folyamatáról.

A felesleges bitek eltávolítása

Természetesen az UTF-8-at vettem alapul. Az első és legnyilvánvalóbb dolog, ami megváltoztatható benne, az az, hogy az egyes bájtokban csökkenteni kell a szolgáltatásbitek számát. Például az UTF-8 első bájtja mindig bármelyikvel kezdődik 0, vagy azzal 11 - előtag 10 Csak a következő bájtok rendelkeznek vele. Cseréljük ki az előtagot 11 on 1, és a következő bájtoknál teljesen eltávolítjuk az előtagokat. Mi fog történni?

0xxxxxxx - 1 bájt
10xxxxxx xxxxxxxx - 2 bájt
110xxxxx xxxxxxxx xxxxxxxx - 3 bájt

Várj, hol van a négybájtos rekord? De már nincs rá szükség – ha három bájtban írunk, most már 21 bit áll rendelkezésre, és ez minden számhoz elegendő 0x10FFFF.

Mit áldoztunk itt? A legfontosabb dolog a karakterhatárok észlelése a puffer tetszőleges helyéről. Nem tudunk tetszőleges bájtra mutatni, és abból megkeresni a következő karakter elejét. Ez a formátumunk korlátozása, de a gyakorlatban erre ritkán van szükség. Általában a kezdetektől fogva át tudjuk futni a puffert (főleg, ha rövid sorokról van szó).

A 2 bájtos nyelvek lefedésével is javult a helyzet: most a kétbájtos formátum 14 bites tartományt ad, és ezek a kódok egészen a 0x3FFF. A kínaiak szerencsétlenek (a karaktereik többnyire 0x4E00 a 0x9FFF), de a grúzok és sok más nép jobban szórakozik – az ő nyelvük is belefér karakterenként 2 bájtba.

Adja meg a kódoló állapotát

Gondoljunk most magukra a vonalak tulajdonságaira. A szótár leggyakrabban azonos ábécé betűkkel írt szavakat tartalmaz, és ez sok más szövegre is igaz. Ezt az ábécét jó lenne egyszer feltüntetni, majd csak a benne lévő betű számát jelezni. Lássuk, segít-e a karakterek elrendezése a Unicode táblában.

Mint fentebb említettük, az Unicode a következőkre oszlik repülőgép 65536 kód mindegyik. De ez nem túl hasznos felosztás (mint már említettük, legtöbbször a nulla síkban vagyunk). Érdekesebb a felosztás blokkok. Ezeknek a tartományoknak már nincs fix hosszúsága, és jelentőségteljesebbek – általában mindegyik ugyanazon ábécé karaktereit kombinálja.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
A bengáli ábécé karaktereit tartalmazó blokk. Sajnos történelmi okokból ez egy példa a nem túl sűrű csomagolásra – 96 karakter kaotikusan szétszórva van 128 blokkkódponton.

A blokkok eleje és mérete mindig a 16 többszöröse – ez egyszerűen a kényelem érdekében történik. Ezenkívül sok blokk olyan értékeken kezdődik és végződik, amelyek 128 vagy akár 256 többszörösei – például az alap cirill ábécé 256 bájtot foglal el 0x0400 a 0x04FF. Ez nagyon kényelmes: ha egyszer mentjük az előtagot 0x04, akkor egy bájtba tetszőleges cirill karakter írható. Igaz, így elveszítjük a lehetőséget, hogy visszatérjünk az ASCII-hez (és általában minden más karakterhez). Ezért ezt tesszük:

  1. Két bájt 10yyyyyy yxxxxxxx nemcsak egy szimbólumot jelöljön számmal yyyyyy yxxxxxxx, hanem változás is aktuális ábécé on yyyyyy y0000000 (azaz minden bitre emlékszünk, kivéve a legkevésbé jelentőseket 7 bit);
  2. Egy bájt 0xxxxxxx ez az aktuális ábécé karaktere. Csak hozzá kell adni az eltoláshoz, amelyre az 1. lépésben emlékeztünk. Bár az ábécét nem változtattuk meg, az eltolás nulla, így megtartottuk az ASCII-vel való kompatibilitást.

Hasonlóképpen a 3 bájtot igénylő kódok esetében:

  1. Három bájt 110yyyyy yxxxxxxx xxxxxxxx jelöljön egy szimbólumot számmal yyyyyy yxxxxxxx xxxxxxxx, változás aktuális ábécé on yyyyyy y0000000 00000000 (mindenre emlékezett, kivéve a fiatalabbakat 15 bit), és jelölje be azt a négyzetet, amelyben most vagyunk hosszú mód (ha visszaállítjuk az ábécét kétbájtosra, akkor ezt a jelzőt visszaállítjuk);
  2. Két bájt 0xxxxxxx xxxxxxxx hosszú módban az aktuális ábécé karaktere. Hasonlóképpen adjuk hozzá az 1. lépés eltolásával. Az egyetlen különbség az, hogy most két bájtot olvasunk (mivel ebbe a módba kapcsoltunk).

Jól hangzik: míg most ugyanabból a 7 bites Unicode tartományból kell karaktereket kódolnunk, az elején 1 plusz bájtot költünk, és karakterenként összesen egy bájtot.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
Az egyik korábbi verzióból működik. Már gyakran veri az UTF-8-at, de van még mit javítani.

Mi rosszabb? Először is van egy feltételünk, nevezetesen aktuális ábécé eltolása és jelölőnégyzetet hosszú mód. Ez tovább korlátoz minket: ma már ugyanazok a karakterek másképp kódolhatók különböző kontextusokban. Például az alkarakterláncok keresését ennek figyelembevételével kell végezni, nem csak a bájtok összehasonlításával. Másodszor, amint megváltoztattuk az ábécét, rossz lett az ASCII-karakterek kódolásával (és ez nem csak a latin ábécé, hanem az alapvető írásjelek is, beleértve a szóközöket is) - ezekhez az ábécét ismét 0-ra kell változtatni, azaz ismét egy plusz bájt (majd még egy, hogy visszatérjünk a lényeghez).

Egy ábécé jó, kettő jobb

Próbáljuk meg kicsit megváltoztatni a bit előtagjainkat, szorítva még egyet a fent leírt háromhoz:

0xxxxxxx — 1 bájt normál módban, 2 hosszú módban
11xxxxxx - 1 bájt
100xxxxx xxxxxxxx - 2 bájt
101xxxxx xxxxxxxx xxxxxxxx - 3 bájt

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat

Most egy kétbájtos rekordban eggyel kevesebb bit áll rendelkezésre – a kód felfelé mutat 0x1FFFés nem 0x3FFF. Azonban még mindig észrevehetően nagyobb, mint a kétbájtos UTF-8 kódokban, a legtöbb általános nyelv még mindig belefér, a legszembetűnőbb veszteség esett hiragana и katakana, szomorúak a japánok.

Mi ez az új kód? 11xxxxxx? Ez egy 64 karakteres kis „rejtvény”, amely kiegészíti a fő ábécénket, ezért hívtam segédnek (kiegészítő) ábécé. Amikor átváltjuk az aktuális ábécét, a régi ábécé egy darabja segédeszközzé válik. Például ASCII-ről cirill betűre váltottunk – a rejtett most már 64 karaktert tartalmaz Latin ábécé, számok, szóköz és vessző (a leggyakrabban előforduló beszúrások nem ASCII szövegekben). Váltson vissza ASCII-re - és a cirill ábécé fő része a segédábécé lesz.

A két ábécéhez való hozzáférésnek köszönhetően rengeteg szöveget tudunk kezelni minimális ábécéváltási költséggel (az írásjelek legtöbbször az ASCII-hez való visszatéréshez vezetnek, de ezt követően sok nem ASCII karaktert kapunk a kiegészítő ábécéből, anélkül újra váltás).

Bónusz: az alábécé előtagja 11xxxxxx és kiválasztja a kezdeti eltolást 0xC0, részleges kompatibilitást kapunk a CP1252-vel. Más szavakkal, sok (de nem mindegyik) nyugat-európai szöveg, amely CP1252-ben van kódolva, ugyanúgy fog kinézni UTF-C-ben.

Itt azonban egy nehézség adódik: hogyan lehet a főábécéből segédet szerezni? Ugyanazt az eltolást meghagyhatod, de - sajnos - itt már az Unicode szerkezet játszik ellenünk. Nagyon gyakran az ábécé fő része nem a blokk elején található (például az orosz „A” nagybetűnek van a kódja 0x0410, bár a cirill betűs blokk ezzel kezdődik 0x0400). Így, miután az első 64 karaktert bevittük a rejtettbe, elveszíthetjük az ábécé farokrészéhez való hozzáférést.

A probléma megoldása érdekében manuálisan végigmentem néhány különböző nyelvnek megfelelő blokkon, és megadtam a segédábécé eltolását a fő ábécén belül. A latin ábécét kivételként általában a base64-hez hasonlóan átrendezték.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat

Utolsó simítások

Végre gondoljuk át, hol lehetne még javítani valamit.

Vegye figyelembe, hogy a formátum 101xxxxx xxxxxxxx xxxxxxxx számok kódolását teszi lehetővé ig 0x1FFFFF, és a Unicode korábban ér véget, at 0x10FFFF. Más szavakkal, az utolsó kódpont a következőképpen lesz ábrázolva 10110000 11111111 11111111. Ezért azt mondhatjuk, hogy ha az első bájt alakú 1011xxxx (ahol xxxx nagyobb, mint 0), akkor ez mást jelent. Például hozzáadhat még 15 karaktert, amelyek folyamatosan elérhetők a kódoláshoz egy bájtban, de úgy döntöttem, hogy másképp csinálom.

Nézzük most azokat a Unicode blokkokat, amelyek három bájtot igényelnek. Alapvetően, mint már említettük, ezek kínai karakterek - de nehéz velük bármit is csinálni, 21 ezer van belőlük. De repült oda a hiragana és a katakana is – és már nincs is belőlük annyi, kevesebb mint kétszáz. És mivel emlékeztünk a japánokra, vannak hangulatjelek is (sőt, sok helyen vannak elszórva a Unicode-ban, de a fő blokkok a tartományban vannak 0x1F300 - 0x1FBFF). Ha arra gondol, hogy most már léteznek olyan hangulatjelek, amelyeket egyszerre több kódpontból állítanak össze (például az emoji ‍‍‍Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat akár 7 kódból áll!), akkor teljesen kár lesz mindegyikre három bájtot költeni (7×3 = 21 bájt egy ikon kedvéért, rémálom).

Ezért kiválasztunk néhány kiválasztott tartományt, amelyek megfelelnek az emojiknak, hiraganának és katakanának, újraszámozzuk őket egy folyamatos listává, és három helyett két bájtba kódoljuk:

1011xxxx xxxxxxxx

Nagyszerű: a fent említett emojiEgy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat, amely 7 kódpontból áll, UTF-8-ban 25 bájtot vesz fel, és beleillesztjük 14 (pontosan két bájt minden kódponthoz). Habr egyébként nem volt hajlandó megemészteni (a régi és az új szerkesztőben sem), így képpel kellett beszúrnom.

Próbáljunk még egy problémát megoldani. Mint emlékszünk, az alapvető ábécé lényegében az magas 6 bit, amit szem előtt tartunk és minden következő dekódolt szimbólum kódjához ragasztunk. A blokkban lévő kínai karakterek esetében 0x4E00 - 0x9FFF, ez vagy 0 vagy 1 bit. Ez nem túl kényelmes: folyamatosan váltanunk kell az ábécét a két érték között (azaz három bájtot kell elköltenünk). De vegye figyelembe, hogy a hosszú módban magából a kódból kivonhatjuk a rövid móddal kódolt karakterek számát (a fent leírt trükkök után ez 10240) - akkor a hieroglifák tartománya eltolódik 0x2600 - 0x77FF, és ebben az esetben a teljes tartományban a legjelentősebb 6 bit (21-ből) egyenlő lesz 0-val. Így a hieroglifák sorozatai két bájtot használnak hieroglifánként (ami optimális egy ilyen nagy tartományhoz), anélkül, hogy ábécé váltókat okozva.

Alternatív megoldások: SCSU, BOCU-1

Az Unicode-szakértők, miután elolvasták a cikk címét, nagy valószínűséggel sietnek emlékeztetni arra, hogy az Unicode szabványok között van Unicode szabványos tömörítési séma (SCSU), amely a cikkben leírthoz nagyon hasonló kódolási módszert ír le.

Bevallom őszintén: csak azután tudtam meg a létezéséről, hogy mélyen belemerültem a döntésem megírásába. Ha tudtam volna a kezdetektől fogva, valószínűleg megpróbáltam volna implementációt írni, ahelyett, hogy saját megközelítéssel dolgoznék.

Az érdekes az, hogy az SCSU olyan ötleteket használ, amelyek nagyon hasonlóak azokhoz, amelyeket én találtam ki (az „ábécék” fogalma helyett „ablakot” használnak, és több van belőlük, mint nekem). Ugyanakkor ennek a formátumnak vannak hátrányai is: kicsit közelebb áll a tömörítési algoritmusokhoz, mint a kódolókhoz. A szabvány különösen sok reprezentációs módszert ad, de nem mondja meg, hogyan kell kiválasztani az optimálisat - ehhez a kódolónak valamilyen heurisztikát kell használnia. Így egy jó csomagolást előállító SCSU kódoló bonyolultabb és körülményesebb lesz, mint az én algoritmusom.

Összehasonlításképpen az SCSU egy viszonylag egyszerű implementációját vittem át JavaScriptre - kódmennyiséget tekintve az UTF-C-mhez hasonlíthatónak bizonyult, de néhány esetben több tíz százalékkal rosszabb lett az eredmény (néha meg is haladhatja, de nem nagyon). Például a héber és görög nyelvű szövegeket UTF-C kódolta 60%-kal jobb, mint az SCSU (valószínűleg kompakt ábécéjük miatt).

Külön hozzáteszem, hogy az SCSU mellett van egy másik módja is a Unicode tömör megjelenítésének - BOCU-1, de a MIME-kompatibilitást célozza meg (amire nem volt szükségem), és egy kicsit más megközelítést alkalmaz a kódoláshoz. Nem értékeltem a hatékonyságát, de úgy tűnik számomra, hogy nem valószínű, hogy magasabb, mint az SCSU.

Lehetséges fejlesztések

Az általam bemutatott algoritmus tervezésénél fogva nem univerzális (valószínűleg itt térnek el leginkább a céljaim az Unicode Konzorcium céljaitól). Említettem már, hogy elsősorban egy feladatra (többnyelvű szótár tárolására előtagfában) fejlesztették ki, és előfordulhat, hogy egyes funkciói nem alkalmasak más feladatokra. De az a tény, hogy ez nem szabvány, plusz lehet - könnyen módosíthatja az igényeinek megfelelően.

Például nyilvánvaló módon megszabadulhat az állapot jelenlététől, állapot nélküli kódolást készíthet - csak ne frissítse a változókat offs, auxOffs и is21Bit a kódolóban és a dekódolóban. Ebben az esetben nem lehet hatékonyan becsomagolni azonos ábécé karaktersorozatait, de garantált lesz, hogy ugyanaz a karakter mindig ugyanazokkal a bájtokkal van kódolva, függetlenül a kontextustól.

Ezenkívül a kódolót egy adott nyelvre szabhatja az alapértelmezett állapot megváltoztatásával - például az orosz szövegekre összpontosítva állítsa be a kódolót és a dekódert az elején offs = 0x0400 и auxOffs = 0. Ennek különösen az állapot nélküli mód esetén van értelme. Általában ez hasonló lesz a régi nyolcbites kódolás használatához, de anélkül, hogy megszüntetné a karakterek beszúrásának lehetőségét az összes Unicode-ból szükség szerint.

Egy másik, korábban említett hátrány, hogy UTF-C kódolású nagy szövegben nincs gyors módja annak, hogy megtaláljuk a tetszőleges bájthoz legközelebb eső karakterhatárt. Ha levágja mondjuk az utolsó 100 bájtot a kódolt pufferből, azzal a kockázattal jár, hogy olyan szemetet kap, amivel nem tud mit kezdeni. A kódolást nem több gigabájtos naplók tárolására tervezték, de ez általában javítható. Byte 0xBF soha nem jelenhet meg első bájtként (de lehet a második vagy a harmadik). Ezért kódoláskor beillesztheti a sorozatot 0xBF 0xBF 0xBF mondjuk 10 KB-onként - akkor, ha határt kell találni, elég lesz a kiválasztott darabot szkennelni, amíg hasonló jelölőt nem talál. Az utolsót követve 0xBF garantáltan egy karakter kezdete lesz. (Dekódoláskor ezt a három bájtos sorozatot természetesen figyelmen kívül kell hagyni.)

Összefoglalva

Ha idáig elolvasta, gratulálok! Remélem, hozzám hasonlóan Ön is tanult valami újat (vagy felfrissítette a memóriáját) az Unicode felépítésével kapcsolatban.

Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat
Demo oldal. A héber nyelv példája bemutatja az UTF-8-cal és az SCSU-val szembeni előnyöket.

A fent leírt kutatás nem tekinthető a szabványok megsértésének. A munkám eredményeivel azonban általában elégedett vagyok, így elégedett vagyok velük részvény: például egy kicsinyített JS-könyvtár csak 1710 bájtot nyom (és természetesen nincsenek függőségei). Mint fentebb említettem, munkái a címen találhatók bemutató oldal (van egy sor szöveg is, amin össze lehet hasonlítani az UTF-8-cal és az SCSU-val).

Végül ismét felhívom a figyelmet azokra az esetekre, amikor UTF-C-t használnak nem éri meg:

  • Ha a sorai elég hosszúak (100-200 karakter). Ebben az esetben érdemes olyan tömörítési algoritmusokat használni, mint a deflate.
  • Ha szükséged van ASCII átlátszóság, vagyis számodra fontos, hogy a kódolt sorozatok ne tartalmazzanak olyan ASCII kódokat, amelyek nem szerepeltek az eredeti karakterláncban. Ennek szükségessége elkerülhető, ha harmadik féltől származó API-kkal való interakció során (például adatbázissal dolgozva) a kódolás eredményét absztrakt bájtkészletként adja át, nem pedig karakterláncként. Ellenkező esetben váratlan sebezhetőséget kaphat.
  • Ha gyorsan meg akarja találni a karakterhatárokat tetszőleges eltolásnál (például ha egy sor egy része sérült). Ez megtehető, de csak a sor elejétől való szkennelésével (vagy az előző részben leírt módosítás alkalmazásával).
  • Ha gyorsan kell műveleteket végrehajtani a karakterláncok tartalmával (rendezni, részkarakterláncokat keresni bennük, összefűzni). Ehhez először a karakterláncokat kell dekódolni, így az UTF-C ezekben az esetekben lassabb lesz, mint az UTF-8 (de gyorsabb, mint a tömörítési algoritmusok). Mivel ugyanazt a karakterláncot mindig ugyanúgy kódolják, a dekódolás pontos összehasonlítása nem szükséges, és bájtonként is elvégezhető.

Frissítés: használó Tyomitch az alábbi megjegyzésekben közzétett egy grafikont, amely kiemeli az UTF-C alkalmazhatósági határait. Azt mutatja, hogy az UTF-C hatékonyabb, mint egy általános célú tömörítési algoritmus (az LZW egy változata), mindaddig, amíg a csomagolt karakterlánc rövidebb. ~140 karakter (megjegyzem azonban, hogy az összehasonlítást egy szövegen végezték el; más nyelvek esetében az eredmény eltérhet).
Egy másik kerékpár: az UTF-30-nál 60-8%-kal kompaktabban tároljuk az Unicode húrokat

Forrás: will.com

Hozzászólás