Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8

Ak ste vývojár a stojíte pred úlohou vybrať kódovanie, Unicode bude takmer vždy tým správnym riešením. Konkrétna metóda reprezentácie závisí od kontextu, ale aj tu najčastejšie existuje univerzálna odpoveď - UTF-8. Dobrá vec na tom je, že vám umožňuje používať všetky znaky Unicode bez výdavkov príliš veľa vo väčšine prípadov veľa bajtov. Je pravda, že pre jazyky, ktoré používajú viac ako len latinskú abecedu, je prinajmenšom „nie príliš veľa“. dva bajty na znak. Môžeme to urobiť lepšie bez návratu k prehistorickým kódovaniam, ktoré nás obmedzujú len na 256 dostupných znakov?

Nižšie navrhujem zoznámiť sa s mojím pokusom odpovedať na túto otázku a implementovať relatívne jednoduchý algoritmus, ktorý vám umožňuje ukladať riadky vo väčšine jazykov sveta bez pridania redundancie, ktorá je v UTF-8.

Vylúčenie zodpovednosti. Okamžite urobím niekoľko dôležitých výhrad: popísané riešenie nie je ponúkané ako univerzálna náhrada za UTF-8, je vhodný iba v úzkom zozname prípadov (viac o nich nižšie) a v žiadnom prípade by sa nemal používať na interakciu s API tretích strán (ktoré o tom ani nevedia). Na kompaktné ukladanie veľkých objemov textových údajov sú najčastejšie vhodné univerzálne kompresné algoritmy (napríklad deflate). Navyše, už v procese vytvárania môjho riešenia som našiel existujúci štandard v samotnom Unicode, ktorý rieši rovnaký problém - je o niečo komplikovanejší (a často horší), ale stále je to akceptovaný štandard, a nie len položený spolu na kolene. Poviem vám aj o ňom.

O Unicode a UTF-8

Na začiatok pár slov o tom, čo to je unicode и UTF-8.

Ako viete, 8-bitové kódovanie bolo populárne. S nimi bolo všetko jednoduché: 256 znakov môže byť očíslovaných číslami od 0 do 255 a čísla od 0 do 255 môžu byť samozrejme reprezentované ako jeden bajt. Ak sa vrátime na úplný začiatok, kódovanie ASCII je úplne obmedzené na 7 bitov, takže najvýznamnejší bit v jeho bajtovej reprezentácii je nula a väčšina 8-bitových kódovaní je s ním kompatibilná (líšia sa iba „horným“ časť, kde najvýznamnejší bit je jedna ).

Ako sa Unicode líši od týchto kódovaní a prečo je s ním spojených toľko špecifických zobrazení - UTF-8, UTF-16 (BE a LE), UTF-32? Urobme si to po poriadku.

Základný štandard Unicode popisuje iba súlad medzi znakmi (av niektorých prípadoch aj jednotlivými zložkami znakov) a ich číslami. A v tomto štandarde je veľa možných čísel - od 0x00 na 0x10FFFF (1 114 112 kusov). Ak by sme chceli do premennej vložiť číslo v takomto rozsahu, nestačil by nám ani 1 ani 2 bajty. A keďže naše procesory nie sú príliš navrhnuté na prácu s trojbajtovými číslami, boli by sme nútení použiť aj 4 bajty na znak! Toto je UTF-32, ale práve pre túto „plytvanie“ nie je tento formát populárny.

Našťastie poradie znakov v rámci Unicode nie je náhodné. Celá ich zostava je rozdelená na 17"lietadlá“, z ktorých každá obsahuje 65536 (0x10000) «kódové body" Koncept „kódového bodu“ je tu jednoduchý číslo znaku, ktoré mu pridelil Unicode. Ale, ako je uvedené vyššie, v Unicode nie sú očíslované len jednotlivé znaky, ale aj ich komponenty a servisné značky (a niekedy vôbec nič nezodpovedá číslu - možno zatiaľ, ale pre nás to nie je také dôležité), takže správnejšie je vždy hovoriť konkrétne o počte samotných čísel a nie o symboloch. V nasledujúcom texte však kvôli stručnosti budem často používať slovo „symbol“, čo znamená pojem „bod kódu“.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Unicode lietadlá. Ako vidíte, väčšina z toho (lietadlá 4 až 13) je stále nevyužitá.

Najpozoruhodnejšie je, že všetka hlavná „buničina“ leží v nulovej rovine, nazýva sa „Základná viacjazyčná rovina". Ak riadok obsahuje text v jednom z moderných jazykov (vrátane čínštiny), neprekročíte túto rovinu. Nemôžete však odrezať ani zvyšok Unicode - napríklad emotikony sa nachádzajú hlavne na konci ďalšie lietadlo,"Doplnková viacjazyčná rovina"(vychádza z 0x10000 na 0x1FFFF). Takže UTF-16 robí toto: všetky znaky spadajúce do neho Základná viacjazyčná rovina, sú zakódované „tak ako sú“ so zodpovedajúcim dvojbajtovým číslom. Niektoré čísla v tomto rozsahu však vôbec neoznačujú konkrétne znaky, ale naznačujú, že po tomto páre bajtov musíme zvážiť ďalší - spojením hodnôt týchto štyroch bajtov dohromady dostaneme číslo, ktoré pokrýva celý platný rozsah Unicode. Táto myšlienka sa nazýva „náhradné páry“ – možno ste o nich už počuli.

Takže UTF-16 vyžaduje dva alebo (vo veľmi zriedkavých prípadoch) štyri bajty na "bod kódu". Je to lepšie ako neustále používať štyri bajty, ale latinka (a ďalšie znaky ASCII) pri kódovaní týmto spôsobom stráca polovicu miesta na nuly. UTF-8 je navrhnutý tak, aby to napravil: ASCII v ňom zaberá, ako predtým, iba jeden bajt; kódy z 0x80 na 0x7FF - dva bajty; od 0x800 na 0xFFFF - tri a od 0x10000 na 0x10FFFF - štyri. Na jednej strane sa latinská abeceda stala dobrou: kompatibilita s ASCII sa vrátila a distribúcia je rovnomernejšie „rozložená“ od 1 do 4 bajtov. Ale iné abecedy ako latinka, bohužiaľ, v porovnaní s UTF-16 nijako neprospievajú a mnohé teraz vyžadujú tri bajty namiesto dvoch – rozsah pokrytý dvojbajtovým záznamom sa zúžil 32-krát, pričom 0xFFFF na 0x7FFa nie je v ňom zahrnutá ani čínština, ani napríklad gruzínčina. Cyrilika a päť ďalších abecied - hurá - šťastie, 2 bajty na znak.

Prečo sa to deje? Pozrime sa, ako UTF-8 predstavuje kódy znakov:
Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Priamo na znázornenie čísel sa tu používajú bity označené symbolom x. Je vidieť, že v dvojbajtovom zázname je len 11 takýchto bitov (zo 16). Vodiace bity tu majú len pomocnú funkciu. V prípade štvorbajtového záznamu je pre číslo kódového bodu alokovaných 21 z 32 bitov - zdalo by sa, že tri bajty (ktoré spolu dávajú 24 bitov) by stačili, no servisné značky zaberajú príliš veľa.

Je to zlé? Nie naozaj. Na jednej strane, ak nám veľmi záleží na priestore, máme kompresné algoritmy, ktoré dokážu jednoducho eliminovať všetku extra entropiu a redundanciu. Na druhej strane, cieľom Unicode bolo poskytnúť čo najuniverzálnejšie kódovanie. Napríklad riadok zakódovaný v UTF-8 môžeme zveriť kódu, ktorý predtým fungoval iba s ASCII, a nebáť sa, že uvidí znak z rozsahu ASCII, ktorý tam v skutočnosti nie je (napokon, v UTF-8 všetky bajty začínajúce od nulového bitu - to je presne to, čo je ASCII). A ak zrazu chceme odrezať malý chvost z veľkého reťazca bez toho, aby sme ho dekódovali od úplného začiatku (alebo obnoviť časť informácií po poškodenom úseku), ľahko nájdeme odsadenie, kde znak začína (stačí na preskočenie bajtov, ktoré majú bitovú predponu 10).

Prečo potom vymýšľať niečo nové?

Zároveň sa občas vyskytnú situácie, keď sú kompresné algoritmy ako deflate zle použiteľné, ale chcete dosiahnuť kompaktné uloženie reťazcov. Osobne som sa pri uvažovaní o stavbe stretol s týmto problémom komprimovaný strom predpôn pre veľký slovník vrátane slov v ľubovoľných jazykoch. Na jednej strane je každé slovo veľmi krátke, takže jeho stlačenie bude neúčinné. Na druhej strane implementácia stromu, o ktorej som uvažoval, bola navrhnutá tak, aby každý bajt uloženého reťazca generoval samostatný vrchol stromu, takže minimalizácia ich počtu bola veľmi užitočná. V mojej knižnici Az.js (Ako v pymorfia2, na ktorom je založený) podobný problém možno vyriešiť jednoducho - struny zabalené do Dawg-slovník, tam uložený v starý dobrý CP1251. Ale ako je ľahké pochopiť, funguje to dobre len pre obmedzenú abecedu - do takého slovníka nemožno pridať riadok v čínštine.

Samostatne by som chcel poznamenať ešte jednu nepríjemnú nuanciu, ktorá vzniká pri použití UTF-8 v takejto dátovej štruktúre. Vyššie uvedený obrázok ukazuje, že keď je znak zapísaný ako dva bajty, bity súvisiace s jeho číslom nie sú za sebou, ale sú oddelené párom bitov 10 v strede: 110xxxxx 10xxxxxx. Z tohto dôvodu, keď spodných 6 bitov druhého bajtu pretečie v kóde znakov (t.j. dôjde k prechodu 1011111110000000), potom sa zmení aj prvý bajt. Ukazuje sa, že písmeno „p“ je označené bajtmi 0xD0 0xBFa ďalšie „r“ už je 0xD1 0x80. V strome prefixov to vedie k rozdeleniu nadradeného uzla na dva - jeden pre predponu 0xD0, a ďalší pre 0xD1 (aj keď celá azbuka sa dala zakódovať iba druhým bajtom).

Čo som dostal

Tvárou v tvár tomuto problému som sa rozhodol precvičiť si hranie hier s bitmi a zároveň sa trochu lepšie zoznámiť so štruktúrou Unicode ako celku. Výsledkom bol formát kódovania UTF-C ("C" pre kompaktné), ktorý neminie viac ako 3 bajty na bod kódu a veľmi často vám umožňuje minúť iba jeden bajt navyše pre celý kódovaný riadok. To vedie k tomu, že na mnohých abecedách bez ASCII sa takéto kódovanie ukazuje ako také O 30-60% kompaktnejšie ako UTF-8.

Uviedol som príklady implementácie kódovacích a dekódovacích algoritmov vo forme JavaScript a Go knižnice, môžete ich voľne použiť vo svojom kóde. Stále však zdôrazním, že tento formát v istom zmysle zostáva „bicyklom“ a neodporúčam ho používať bez toho, aby ste si uvedomili, prečo to potrebujete. Toto je stále skôr experiment ako vážne „vylepšenie UTF-8“. Napriek tomu je tam kód napísaný úhľadne, stručne, s veľkým množstvom komentárov a testovaním.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Výsledky testov a porovnanie s UTF-8

Tiež som to urobil ukážková stránka, kde môžete vyhodnotiť výkon algoritmu a potom vám poviem viac o jeho princípoch a procese vývoja.

Odstránenie nadbytočných bitov

Ako základ som samozrejme bral UTF-8. Prvá a najzreteľnejšia vec, ktorú je možné v ňom zmeniť, je zníženie počtu obslužných bitov v každom byte. Napríklad prvý bajt v UTF-8 vždy začína buď 0, alebo s 11 - predpona 10 Majú ho iba nasledujúce bajty. Nahradíme predponu 11 na 1a pre ďalšie bajty predpony úplne odstránime. Čo sa bude diať?

0xxxxxxx — 1 bajt
10xxxxxx xxxxxxxx - 2 bajty
110xxxxx xxxxxxxx xxxxxxxx - 3 bajty

Počkaj, kde je ten štvorbajtový záznam? Ale už to nie je potrebné - pri zápise v troch bajtoch máme k dispozícii 21 bitov a to stačí pre všetky čísla do 0x10FFFF.

Čo sme tu obetovali? Najdôležitejšia je detekcia hraníc znakov z ľubovoľného miesta vo vyrovnávacej pamäti. Nemôžeme ukázať na ľubovoľný bajt a nájsť z neho začiatok ďalšieho znaku. Toto je obmedzenie nášho formátu, ale v praxi je to zriedka potrebné. Zvyčajne sme schopní prebehnúť buffer od úplného začiatku (najmä ak ide o krátke linky).

Situácia s pokrytím jazykov s 2 bajtmi sa tiež zlepšila: teraz dvojbajtový formát poskytuje rozsah 14 bitov, a to sú kódy až do 0x3FFF. Číňania majú smolu (ich znaky sa väčšinou pohybujú od 0x4E00 na 0x9FFF), ale Gruzínci a mnoho ďalších národov sa baví viac - ich jazyky sa tiež zmestia do 2 bajtov na znak.

Zadajte stav kódovača

Zamyslime sa teraz nad vlastnosťami samotných čiar. Slovník najčastejšie obsahuje slová písané znakmi rovnakej abecedy a platí to aj pre mnohé iné texty. Bolo by dobré označiť túto abecedu raz a potom uviesť iba číslo písmena v nej. Uvidíme, či nám pomôže usporiadanie znakov v tabuľke Unicode.

Ako je uvedené vyššie, Unicode sa delí na lietadlo 65536 kódov každý. Ale to nie je veľmi užitočné delenie (ako už bolo povedané, najčastejšie sme v nulovej rovine). Zaujímavejšie je delenie podľa bloky. Tieto rozsahy už nemajú pevnú dĺžku a sú zmysluplnejšie – spravidla každý kombinuje znaky z rovnakej abecedy.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Blok obsahujúci znaky bengálskej abecedy. Bohužiaľ, z historických dôvodov ide o príklad nie príliš hustého balenia – 96 znakov je chaoticky roztrúsených po 128 bodoch blokového kódu.

Začiatky blokov a ich veľkosti sú vždy násobky 16 - to sa robí jednoducho pre pohodlie. Okrem toho veľa blokov začína a končí na hodnotách, ktoré sú násobkami 128 alebo dokonca 256 - napríklad základná azbuka zaberá 256 bajtov od 0x0400 na 0x04FF. To je celkom výhodné: ak predponu raz uložíme 0x04, potom je možné do jedného bajtu zapísať ľubovoľný znak cyriliky. Pravdaže, takto prídeme o možnosť vrátiť sa k ASCII (a k akýmkoľvek iným znakom všeobecne). Preto robíme toto:

  1. Dva bajty 10yyyyyy yxxxxxxx nielen označovať symbol číslom yyyyyy yxxxxxxx, ale aj zmeniť aktuálna abeceda na yyyyyy y0000000 (t. j. pamätáme si všetky bity okrem tých najmenej významných Bit 7);
  2. Jeden bajt 0xxxxxxx toto je znak súčasnej abecedy. Stačí ho pridať k posunu, ktorý sme si zapamätali v kroku 1. Aj keď sme nezmenili abecedu, posun je nulový, takže sme zachovali kompatibilitu s ASCII.

Podobne pre kódy vyžadujúce 3 bajty:

  1. Tri bajty 110yyyyy yxxxxxxx xxxxxxxx označte symbol číslom yyyyyy yxxxxxxx xxxxxxxx, zmeniť aktuálna abeceda na yyyyyy y0000000 00000000 (pamätal si všetko okrem mladších Bit 15) a začiarknite políčko, v ktorom sa práve nachádzame dlhý režim (pri zmene abecedy späť na dvojbajtovú tento príznak vynulujeme);
  2. Dva bajty 0xxxxxxx xxxxxxxx v dlhom režime je to znak aktuálnej abecedy. Podobne to pridáme s posunom z kroku 1. Rozdiel je len v tom, že teraz čítame dva bajty (pretože sme prešli do tohto režimu).

Znie to dobre: ​​zatiaľ čo teraz potrebujeme zakódovať znaky z rovnakého 7-bitového rozsahu Unicode, minieme na začiatku 1 bajt navyše a celkovo jeden bajt na znak.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Pracuje z jednej zo starších verzií. Už teraz často prekonáva UTF-8, ale stále je čo zlepšovať.

čo je horšie? Po prvé, máme podmienku, a to aktuálny posun abecedy a začiarkavacie políčko dlhý režim. To nás ešte viac obmedzuje: teraz môžu byť rovnaké znaky kódované odlišne v rôznych kontextoch. Napríklad vyhľadávanie podreťazcov bude musieť byť vykonané s ohľadom na túto skutočnosť a nielen porovnávaním bajtov. Po druhé, akonáhle sme zmenili abecedu, pokazilo sa to s kódovaním ASCII znakov (a to nie je len latinka, ale aj základná interpunkcia vrátane medzier) - vyžadujú zmenu abecedy znova na 0, tj. opäť jeden bajt navyše (a potom ďalší, aby sme sa vrátili k nášmu hlavnému bodu).

Jedna abeceda je dobrá, dve sú lepšie

Pokúsme sa trochu zmeniť naše bitové predpony a vtlačiť ešte jednu k trom opísaným vyššie:

0xxxxxxx — 1 bajt v normálnom režime, 2 v dlhom režime
11xxxxxx — 1 bajt
100xxxxx xxxxxxxx - 2 bajty
101xxxxx xxxxxxxx xxxxxxxx - 3 bajty

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8

Teraz je v dvojbajtovom zázname o jeden dostupný bit menej - kód ukazuje až 0x1FFFA nie 0x3FFF. Stále je však výrazne väčší ako v dvojbajtových kódoch UTF-8, väčšina bežných jazykov sa stále zmestí, najvýraznejšia strata vypadla hiragana и katakana, Japonci sú smutní.

Aký je tento nový kód? 11xxxxxx? Toto je malá „skrýša“ s veľkosťou 64 znakov, ktorá dopĺňa našu hlavnú abecedu, preto som ju nazval pomocnou (pomocný) abeceda. Keď prepneme aktuálnu abecedu, kúsok starej abecedy sa stane pomocnou. Napríklad sme prešli z ASCII na azbuku – schránka teraz obsahuje 64 znakov Latinská abeceda, čísla, medzera a čiarka (najčastejšie vkladanie do neASCII textov). Prešli sme späť na ASCII - a hlavná časť azbuky sa stane pomocnou abecedou.

Vďaka prístupu k dvom abecedám dokážeme spracovať veľké množstvo textov s minimálnymi nákladmi na prepínanie abecied (interpunkcia najčastejšie povedie k návratu k ASCII, ale potom získame veľa neASCII znakov z doplnkovej abecedy, bez opätovné prepnutie).

Bonus: predpona podabecedy 11xxxxxx a výber jeho počiatočného posunu byť 0xC0, získame čiastočnú kompatibilitu s CP1252. Inými slovami, mnohé (ale nie všetky) západoeurópske texty zakódované v CP1252 budú vyzerať rovnako v UTF-C.

Tu však nastáva problém: ako získať pomocnú z hlavnej abecedy? Môžete ponechať rovnaký offset, ale - bohužiaľ - tu už hrá štruktúra Unicode proti nám. Hlavná časť abecedy sa veľmi často nenachádza na začiatku bloku (napríklad ruské veľké „A“ má kód 0x0410, hoci cyrilský blok začína s 0x0400). Keď teda vezmeme prvých 64 znakov do skrýše, môžeme stratiť prístup k zadnej časti abecedy.

Aby som tento problém vyriešil, manuálne som prešiel cez niektoré bloky zodpovedajúce rôznym jazykom a určil som pre ne posun pomocnej abecedy v rámci hlavnej. Latinská abeceda bola ako výnimka vo všeobecnosti preusporiadaná ako základ64.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8

Záverečné úpravy

Poďme sa už konečne zamyslieť, kde ešte môžeme niečo zlepšiť.

Všimnite si, že formát 101xxxxx xxxxxxxx xxxxxxxx umožňuje kódovať čísla až do 0x1FFFFFa Unicode končí skôr, o 0x10FFFF. Inými slovami, posledný bod kódu bude reprezentovaný ako 10110000 11111111 11111111. Preto môžeme povedať, že ak je prvý bajt vo forme 1011xxxx (kde xxxx väčšie ako 0), potom to znamená niečo iné. Môžete tam napríklad pridať ďalších 15 znakov, ktoré sú neustále k dispozícii na kódovanie do jedného bajtu, ale rozhodol som sa to urobiť inak.

Pozrime sa teraz na tie bloky Unicode, ktoré vyžadujú tri bajty. V podstate, ako už bolo spomenuté, ide o čínske znaky - ale je ťažké s nimi niečo urobiť, je ich 21 tisíc. Ale letela tam aj hiragana a katakana – a už ich nie je toľko, menej ako dvesto. A keďže sme si spomenuli na Japoncov, existujú aj emotikony (v skutočnosti sú v Unicode roztrúsené na mnohých miestach, ale hlavné bloky sú v dosahu 0x1F300 - 0x1FBFF). Ak sa zamyslíte nad tým, že teraz existujú emotikony, ktoré sú zostavené z niekoľkých kódových bodov naraz (napríklad emodži ‍‍‍Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8 pozostáva až zo 7 kódov!), potom sa stáva úplnou hanbou minúť na každý tri bajty (7×3 = 21 bajtov kvôli jednej ikone, nočná mora).

Preto vyberieme niekoľko vybraných rozsahov zodpovedajúcich emoji, hiragane a katakane, prečíslujeme ich do jedného súvislého zoznamu a zakódujeme ako dva bajty namiesto troch:

1011xxxx xxxxxxxx

Skvelé: vyššie uvedené emotikonyĎalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8, pozostávajúci zo 7 kódových bodov, zaberá 8 bajtov v UTF-25 a zmestíme sa doň 14 (presne dva bajty pre každý kódový bod). Mimochodom, Habr to odmietol stráviť (v starom aj v novom editore), tak som to musel vložiť aj s obrázkom.

Skúsme vyriešiť ešte jeden problém. Ako si pamätáme, základná abeceda je v podstate vysoký 6 bitov, ktorý máme na pamäti a lepíme na kód každého ďalšieho dekódovaného symbolu. V prípade čínskych znakov, ktoré sú v bloku 0x4E00 - 0x9FFF, je to buď bit 0 alebo 1. To nie je príliš pohodlné: budeme musieť neustále prepínať abecedu medzi týmito dvoma hodnotami (t. j. minúť tri bajty). Všimnite si však, že v dlhom režime môžeme od samotného kódu odpočítať počet znakov, ktoré zakódujeme pomocou krátkeho režimu (po všetkých vyššie opísaných trikoch je to 10240 XNUMX) - potom sa rozsah hieroglyfov posunie na 0x2600 - 0x77FFa v tomto prípade v celom tomto rozsahu bude najvýznamnejších 6 bitov (z 21) rovných 0. Sekvencie hieroglyfov teda budú používať dva bajty na hieroglyf (čo je optimálne pre taký veľký rozsah), bez spôsobuje prepínanie abecedy.

Alternatívne riešenia: SCSU, BOCU-1

Odborníci na Unicode, ktorí si práve prečítali názov článku, vám s najväčšou pravdepodobnosťou pripomenie, že priamo medzi štandardmi Unicode existuje Štandardná kompresná schéma pre Unicode (SCSU), ktorý popisuje metódu kódovania veľmi podobnú tej, ktorá je opísaná v článku.

Úprimne priznávam: o jej existencii som sa dozvedel až potom, čo som bol hlboko ponorený do písania svojho rozhodnutia. Keby som o tom vedel od začiatku, pravdepodobne by som sa pokúsil napísať implementáciu namiesto toho, aby som prišiel s vlastným prístupom.

Zaujímavé je, že SCSU používa nápady veľmi podobné tým, na ktoré som prišiel sám (namiesto konceptu „abecedy“ používajú „okná“ a je ich k dispozícii viac, ako mám ja). Zároveň má tento formát aj nevýhody: je o niečo bližšie k kompresným algoritmom ako kódovacím. Predovšetkým štandard poskytuje veľa reprezentačných metód, ale nehovorí, ako zvoliť optimálnu - na to musí kodér použiť nejaký druh heuristiky. Takže kódovač SCSU, ktorý vytvára dobré balenie, bude zložitejší a ťažkopádnejší ako môj algoritmus.

Pre porovnanie som do JavaScriptu preniesol pomerne jednoduchú implementáciu SCSU - z hľadiska objemu kódu mi to vyšlo porovnateľne s mojím UTF-C, no v niektorých prípadoch bol výsledok o desiatky percent horší (niekedy ho možno prekročí, ale nie príliš). Napríklad texty v hebrejčine a gréčtine boli kódované pomocou UTF-C O 60 % lepšie ako SCSU (pravdepodobne kvôli ich kompaktným abecedám).

Samostatne dodám, že okrem SCSU existuje aj iný spôsob, ako kompaktne reprezentovať Unicode - BOCU-1, ale zameriava sa na MIME kompatibilitu (čo som nepotreboval) a má trochu iný prístup ku kódovaniu. Nehodnotil som jeho účinnosť, ale zdá sa mi, že je nepravdepodobné, že by bol vyšší ako SCSU.

Možné vylepšenia

Algoritmus, ktorý som predstavil, nie je dizajnovo univerzálny (pravdepodobne sa moje ciele najviac rozchádzajú s cieľmi Unicode Consortium). Už som spomenul, že bol vyvinutý primárne pre jednu úlohu (ukladanie viacjazyčného slovníka do stromu prefixov) a niektoré jeho funkcie nemusia byť vhodné pre iné úlohy. Ale skutočnosť, že to nie je štandard, môže byť plus - môžete ho ľahko upraviť podľa svojich potrieb.

Napríklad, zrejmým spôsobom sa môžete zbaviť prítomnosti stavu, vytvoriť bezstavové kódovanie – jednoducho neaktualizujte premenné offs, auxOffs и is21Bit v kódovači a dekodéri. V tomto prípade nebude možné efektívne zbaliť sekvencie znakov rovnakej abecedy, ale bude zaručené, že rovnaký znak bude vždy zakódovaný rovnakými bajtmi, bez ohľadu na kontext.

Okrem toho si môžete prispôsobiť kódovač konkrétnemu jazyku zmenou predvoleného stavu - napríklad zameraním na ruské texty, nastavte kódovač a dekodér na začiatku offs = 0x0400 и auxOffs = 0. To dáva zmysel najmä v prípade bezstavového režimu. Vo všeobecnosti to bude podobné ako pri použití starého osembitového kódovania, ale bez odstránenia možnosti vkladať znaky zo všetkých Unicode podľa potreby.

Ďalšou nevýhodou uvedenou vyššie je, že vo veľkom texte kódovanom v UTF-C neexistuje rýchly spôsob, ako nájsť hranicu znakov najbližšie k ľubovoľnému bajtu. Ak odrežete posledných povedzme 100 bajtov z kódovanej vyrovnávacej pamäte, riskujete, že dostanete odpad, s ktorým nemôžete nič robiť. Kódovanie nie je určené na ukladanie multi-gigabajtových protokolov, ale vo všeobecnosti sa to dá opraviť. Byte 0xBF sa nikdy nesmie objaviť ako prvý bajt (ale môže byť druhý alebo tretí). Preto pri kódovaní môžete vložiť sekvenciu 0xBF 0xBF 0xBF každých, povedzme, 10 KB - potom, ak potrebujete nájsť hranicu, bude stačiť skenovať vybraný kus, kým sa nenájde podobná značka. Po poslednom 0xBF je zaručene začiatok postavy. (Pri dekódovaní bude potrebné túto postupnosť troch bajtov samozrejme ignorovať.)

Sčítanie

Ak ste sa dočítali až sem, gratulujeme! Dúfam, že ste sa rovnako ako ja dozvedeli niečo nové (alebo si osviežili pamäť) o štruktúre Unicode.

Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8
Demo stránka. Príklad hebrejčiny ukazuje výhody oproti UTF-8 aj SCSU.

Vyššie popísaný výskum by sa nemal považovať za zásah do noriem. S výsledkami svojej práce som však celkovo spokojný, takže mám z nich radosť podiel: napríklad minifikovaná knižnica JS váži iba 1710 bajtov (a samozrejme nemá žiadne závislosti). Ako som spomenul vyššie, jej tvorbu nájdete na ukážková stránka (existuje aj sada textov, na ktorých sa dá porovnať s UTF-8 a SCSU).

Nakoniec ešte raz upozorním na prípady, v ktorých sa používa UTF-C nestojí za to:

  • Ak sú vaše riadky dostatočne dlhé (100-200 znakov). V tomto prípade by ste mali premýšľať o použití kompresných algoritmov, ako je deflate.
  • Ak potrebuješ ASCII transparentnosť, to znamená, že je pre vás dôležité, aby kódované sekvencie neobsahovali ASCII kódy, ktoré neboli v pôvodnom reťazci. Tejto potrebe sa dá vyhnúť, ak pri interakcii s rozhraniami API tretích strán (napríklad pri práci s databázou) odošlete výsledok kódovania ako abstraktnú množinu bajtov, a nie ako reťazce. V opačnom prípade riskujete neočakávané zraniteľné miesta.
  • Ak chcete rýchlo nájsť hranice znakov s ľubovoľným posunom (napríklad, keď je časť riadku poškodená). Dá sa to urobiť, ale iba skenovaním riadku od začiatku (alebo použitím úpravy opísanej v predchádzajúcej časti).
  • Ak potrebujete rýchlo vykonávať operácie s obsahom reťazcov (triediť ich, hľadať v nich podreťazce, spájať). Vyžaduje si to najskôr dekódovanie reťazcov, takže UTF-C bude v týchto prípadoch pomalšie ako UTF-8 (ale rýchlejšie ako kompresné algoritmy). Keďže rovnaký reťazec je vždy zakódovaný rovnakým spôsobom, nie je potrebné presné porovnanie dekódovania a možno ho vykonať po byte.

Update: užívateľ Tyomitch v komentároch nižšie zverejnil graf zvýrazňujúci limity použiteľnosti UTF-C. Ukazuje, že UTF-C je efektívnejšie ako univerzálny kompresný algoritmus (variácia LZW), pokiaľ je zbalený reťazec kratší. ~140 znakov (Upozorňujem však, že porovnanie bolo vykonané na jednom texte, pre iné jazyky sa môže výsledok líšiť).
Ďalší bicykel: struny Unicode skladujeme o 30-60% kompaktnejšie ako UTF-8

Zdroj: hab.com

Pridať komentár