Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8

Pokud jste vývojáři a stojíte před úkolem vybrat kódování, pak bude Unicode téměř vždy tím správným řešením. Konkrétní způsob reprezentace závisí na kontextu, ale i zde nejčastěji existuje univerzální odpověď - UTF-8. Dobrá věc na tom je, že vám umožňuje používat všechny znaky Unicode bez utrácení příliš mnoho ve většině případů hodně bajtů. Je pravda, že pro jazyky, které používají více než jen latinskou abecedu, je „ne příliš mnoho“ přinejmenším dva bajty na znak. Můžeme to udělat lépe, aniž bychom se vrátili k prehistorickým kódování, které nás omezuje na pouhých 256 dostupných znaků?

Níže navrhuji, abyste se seznámili s mým pokusem odpovědět na tuto otázku a implementovali relativně jednoduchý algoritmus, který vám umožní ukládat řádky ve většině jazyků světa bez přidání redundance, která je v UTF-8.

Prohlášení. Okamžitě udělám několik důležitých výhrad: popsané řešení není nabízeno jako univerzální náhrada za UTF-8, je vhodný pouze v úzkém seznamu případů (více o nich níže) a v žádném případě by neměl být používán pro interakci s API třetích stran (kteří o tom ani nevědí). Pro kompaktní ukládání velkých objemů textových dat jsou nejčastěji vhodné univerzální kompresní algoritmy (například deflate). Navíc jsem již v procesu tvorby svého řešení našel existující standard v samotném Unicode, který řeší stejný problém – je sice poněkud složitější (a často horší), ale přesto je to uznávaný standard, a to nejen kladený spolu na koleni. Také vám o něm povím.

O Unicode a UTF-8

Na začátek pár slov o tom, co to je Unicode и UTF-8.

Jak víte, 8bitové kódování bývalo populární. S nimi bylo vše jednoduché: 256 znaků lze očíslovat čísly od 0 do 255 a čísla od 0 do 255 lze samozřejmě reprezentovat jako jeden bajt. Pokud se vrátíme na úplný začátek, kódování ASCII je zcela omezeno na 7 bitů, takže nejvýznamnější bit v jeho bytové reprezentaci je nula a většina 8bitových kódování je s ním kompatibilní (liší se pouze „horním“ část, kde nejvýznamnější bit je jedna ).

Jak se Unicode liší od těchto kódování a proč je s ním spojeno tolik konkrétních reprezentací - UTF-8, UTF-16 (BE a LE), UTF-32? Udělejme to po pořádku.

Základní standard Unicode popisuje pouze shodu mezi znaky (a v některých případech i jednotlivými složkami znaků) a jejich čísly. A v této normě je spousta možných čísel - od 0x00 na 0x10FFFF (1 114 112 kusů). Pokud bychom chtěli do proměnné vložit číslo v takovém rozsahu, nestačil by nám 1 ani 2 bajty. A protože naše procesory nejsou příliš navrženy pro práci s tříbajtovými čísly, byli bychom nuceni použít i 4 bajty na znak! Toto je UTF-32, ale právě kvůli této „plýtvání“ není tento formát populární.

Naštěstí pořadí znaků v Unicode není náhodné. Celá jejich sada je rozdělena na 17"letadla", z nichž každá obsahuje 65536 (0x10000) "kódové body" Koncept „kódového bodu“ je zde jednoduchý číslo znaku, které je přiřazeno Unicode. Ale jak je uvedeno výše, v Unicode se nečíslují jen jednotlivé znaky, ale také jejich součásti a servisní značky (a někdy číslu neodpovídá vůbec nic - možná prozatím, ale pro nás to není tak důležité), takže správnější je vždy mluvit konkrétně o počtu samotných čísel a ne o symbolech. Nicméně v následujícím budu kvůli stručnosti často používat slovo „symbol“, což znamená výraz „bod kódu“.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Unicode letadla. Jak vidíte, většina z toho (letadla 4 až 13) je stále nevyužitá.

Nejpozoruhodnější je, že veškerá hlavní „buničina“ leží v nulové rovině, nazývá se „Základní vícejazyčná rovina". Pokud řádek obsahuje text v některém z moderních jazyků (včetně čínštiny), nepřekročíte tuto rovinu. Nemůžete však odříznout ani zbytek Unicode - například emotikony se nacházejí hlavně na konci další letadlo,"Doplňková vícejazyčná rovina"(vychází z 0x10000 na 0x1FFFF). UTF-16 tedy dělá toto: všechny znaky spadající do něj Základní vícejazyčná rovina, jsou zakódovány „tak jak jsou“ s odpovídajícím dvoubajtovým číslem. Některá čísla v tomto rozsahu však vůbec neoznačují konkrétní znaky, ale naznačují, že po této dvojici bajtů musíme zvážit další - kombinací hodnot těchto čtyř bajtů dohromady získáme číslo, které pokrývá celý platný rozsah Unicode. Tato myšlenka se nazývá „náhradní páry“ – možná jste o nich slyšeli.

UTF-16 tedy vyžaduje dva nebo (ve velmi vzácných případech) čtyři bajty na „bod kódu“. To je lepší než používat neustále čtyři bajty, ale latinka (a další znaky ASCII) při kódování tímto způsobem plýtvá polovinou místa na nuly. UTF-8 je navrženo, aby to napravilo: ASCII v něm zabírá, jako dříve, pouze jeden bajt; kódy od 0x80 na 0x7FF - dva bajty; z 0x800 na 0xFFFF - tři a od 0x10000 na 0x10FFFF - čtyři. Na jedné straně se latinská abeceda stala dobrou: vrátila se kompatibilita s ASCII a distribuce je rovnoměrněji „rozložena“ od 1 do 4 bajtů. Ale jiné abecedy než latinka, bohužel, ve srovnání s UTF-16 nijak neprospívají a mnohé nyní vyžadují tři bajty místo dvou - rozsah pokrytý dvoubajtovým záznamem se zúžil 32krát, 0xFFFF na 0x7FF, a není v něm zahrnuta ani čínština, ani například gruzínština. Cyrilice a pět dalších abeced - hurá - štěstí, 2 bajty na znak.

Proč se to děje? Podívejme se, jak UTF-8 představuje kódy znaků:
Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Přímo k reprezentaci čísel se zde používají bity označené symbolem x. Je vidět, že ve dvoubajtovém záznamu je takových bitů pouze 11 (ze 16). Náběhové bity zde mají pouze pomocnou funkci. V případě čtyřbajtového záznamu je pro číslo kódového bodu alokováno 21 z 32 bitů - zdálo by se, že tři bajty (které dávají dohromady 24 bitů) by stačily, ale značky služeb žerou příliš mnoho.

Je to špatné? Spíš ne. Na jedné straně, pokud nám hodně záleží na prostoru, máme kompresní algoritmy, které mohou snadno eliminovat veškerou nadbytečnou entropii a redundanci. Na druhou stranu, cílem Unicode bylo poskytnout co nejuniverzálnější kódování. Řádek zakódovaný v UTF-8 můžeme například svěřit kódu, který dříve fungoval pouze s ASCII, a nebát se, že uvidí znak z rozsahu ASCII, který tam ve skutečnosti není (ostatně v UTF-8 všechny bajtů začínajících od nulového bitu - přesně to je ASCII). A pokud najednou chceme z velkého řetězce odříznout malý ocásek, aniž bychom ho od začátku dekódovali (nebo obnovit část informace po poškozeném úseku), snadno najdeme odsazení, kde postava začíná (stačí pro přeskočení bajtů, které mají bitovou předponu 10).

Proč potom vymýšlet něco nového?

Zároveň občas nastanou situace, kdy jsou kompresní algoritmy jako deflate špatně použitelné, ale chcete dosáhnout kompaktního uložení řetězců. Osobně jsem se s tímto problémem setkal při úvahách o stavbě komprimovaný strom předpon pro velký slovník obsahující slova v libovolných jazycích. Na jednu stranu je každé slovo velmi krátké, takže jeho komprimace bude neúčinná. Na druhou stranu implementace stromu, kterou jsem zvažoval, byla navržena tak, že každý bajt uloženého řetězce generoval samostatný vrchol stromu, takže minimalizace jejich počtu byla velmi užitečná. V mé knihovně Az.js (Jako v pymorfie2, na kterém je založeno) lze podobný problém vyřešit jednoduše - strings packed into DAWG-slovník, tam uložený starý dobrý CP1251. Jak je však snadné pochopit, funguje to dobře pouze pro omezenou abecedu - do takového slovníku nelze přidat řádek v čínštině.

Samostatně bych rád poznamenal ještě jednu nepříjemnou nuanci, která vzniká při použití UTF-8 v takové datové struktuře. Obrázek výše ukazuje, že když je znak zapsán jako dva bajty, bity související s jeho číslem nejdou za sebou, ale jsou odděleny dvojicí bitů 10 uprostřed: 110xxxxx 10xxxxxx. Z tohoto důvodu, když spodních 6 bitů druhého bajtu přeteče v kódu znaku (tj. dojde k přechodu 1011111110000000), pak se změní i první bajt. Ukazuje se, že písmeno „p“ je označeno bajty 0xD0 0xBFa další „r“ už je 0xD1 0x80. Ve stromu prefixů to vede k rozdělení nadřazeného uzlu na dva - jeden pro prefix 0xD0a další pro 0xD1 (ačkoli celá azbuka mohla být zakódována pouze druhým bytem).

Co jsem dostal

Tváří v tvář tomuto problému jsem se rozhodl procvičit hraní her s bity a zároveň se o něco lépe seznámit se strukturou Unicode jako celku. Výsledkem byl formát kódování UTF-C ("C" pro kompaktní), který neutratí více než 3 bajty na bod kódu a velmi často vám umožňuje utrácet pouze jeden bajt navíc pro celý kódovaný řádek. To vede k tomu, že na mnoha ne-ASCII abecedách se takové kódování ukáže jako takové O 30-60% kompaktnější než UTF-8.

Ve formuláři jsem uvedl příklady implementace kódovacích a dekódovacích algoritmů Knihovny JavaScript a Go, můžete je volně používat ve svém kódu. Stále ale zdůrazňuji, že v jistém smyslu tento formát zůstává „kolem“ a nedoporučuji jej používat aniž byste si uvědomili, proč to potřebujete. Je to stále spíše experiment než seriózní „vylepšení UTF-8“. Přesto je tam kód napsaný úhledně, stručně, s velkým množstvím komentářů a testovacím pokrytím.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Výsledky testů a srovnání s UTF-8

Také jsem to udělal ukázková stránka, kde můžete vyhodnotit výkon algoritmu, a poté vám řeknu více o jeho principech a procesu vývoje.

Eliminace nadbytečných bitů

Jako základ jsem samozřejmě bral UTF-8. První a nejzřejmější věc, kterou v něm lze změnit, je snížit počet servisních bitů v každém bajtu. Například první bajt v UTF-8 vždy začíná buď 0, nebo s 11 - předpona 10 Mají ji pouze následující bajty. Nahradíme předponu 11 na 1a pro další bajty předpony úplně odstraníme. Co se bude dít?

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

Počkat, kde je ten čtyřbajtový záznam? Už to ale není potřeba – při zápisu ve třech bytech máme nyní k dispozici 21 bitů a to stačí pro všechna čísla do 0x10FFFF.

Co jsme tady obětovali? Nejdůležitější je detekce hranic znaků z libovolného místa ve vyrovnávací paměti. Nemůžeme ukázat na libovolný bajt a najít z něj začátek dalšího znaku. Toto je omezení našeho formátu, ale v praxi je to zřídka nutné. Obvykle jsme schopni proběhnout buffer od samého začátku (zejména pokud jde o krátké linky).

Situace s pokrytím jazyků 2 bajty se také zlepšila: nyní dvoubajtový formát poskytuje rozsah 14 bitů, a to jsou kódy až 0x3FFF. Číňané mají smůlu (jejich znaky se většinou pohybují od 0x4E00 na 0x9FFF), ale Gruzínci a mnoho dalších národů se baví víc - jejich jazyky se také vejdou do 2 bajtů na znak.

Zadejte stav kodéru

Pojďme se nyní zamyslet nad vlastnostmi samotných čar. Slovník obsahuje nejčastěji slova psaná znaky stejné abecedy a to platí i pro mnoho dalších textů. Bylo by dobré označit tuto abecedu jednou a poté uvést pouze číslo písmene v ní. Uvidíme, zda nám pomůže uspořádání znaků v Unicode tabulce.

Jak bylo uvedeno výše, Unicode se dělí na letadlo 65536 kódů každý. To ale není příliš užitečné dělení (jak již bylo řečeno, nejčastěji jsme v nulové rovině). Zajímavější je dělení podle bloky. Tyto rozsahy již nemají pevnou délku a jsou smysluplnější – každý zpravidla kombinuje znaky ze stejné abecedy.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Blok obsahující znaky bengálské abecedy. Bohužel z historických důvodů jde o příklad nepříliš hustého balení – 96 znaků je chaoticky roztroušeno po 128 bodech blokového kódu.

Začátky bloků a jejich velikosti jsou vždy násobky 16 - to je provedeno jednoduše pro pohodlí. Navíc mnoho bloků začíná a končí na hodnotách, které jsou násobky 128 nebo dokonce 256 – například základní azbuka zabírá 256 bajtů od 0x0400 na 0x04FF. To je docela pohodlné: pokud uložíme předponu jednou 0x04, pak lze do jednoho bajtu zapsat libovolný znak azbuky. Pravda, tímto způsobem přijdeme o možnost vrátit se k ASCII (a obecně k jakýmkoliv dalším znakům). Proto děláme toto:

  1. Dva bajty 10yyyyyy yxxxxxxx nejen označovat symbol číslem yyyyyy yxxxxxxx, ale také změnit aktuální abeceda na yyyyyy y0000000 (tj. pamatujeme si všechny bity kromě těch nejméně významných Bit 7);
  2. Jeden bajt 0xxxxxxx to je znak současné abecedy. Je třeba jej přidat k offsetu, který jsme si zapamatovali v kroku 1. I když jsme nezměnili abecedu, offset je nulový, takže jsme zachovali kompatibilitu s ASCII.

Podobně pro kódy vyžadující 3 bajty:

  1. Tři bajty 110yyyyy yxxxxxxx xxxxxxxx označte symbol číslem yyyyyy yxxxxxxx xxxxxxxx, změna aktuální abeceda na yyyyyy y0000000 00000000 (pamatoval si všechno kromě mladších Bit 15) a zaškrtněte políčko, ve kterém se nyní nacházíme dlouho režim (při změně abecedy zpět na dvoubajtovou tento příznak resetujeme);
  2. Dva bajty 0xxxxxxx xxxxxxxx v dlouhém režimu je to znak aktuální abecedy. Podobně jej přidáme s offsetem z kroku 1. Jediný rozdíl je v tom, že nyní čteme dva byty (protože jsme přešli do tohoto režimu).

Zní to dobře: zatímco nyní potřebujeme zakódovat znaky ze stejného 7bitového rozsahu Unicode, utratíme na začátku 1 bajt navíc a celkem jeden bajt na znak.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Práce z jedné z dřívějších verzí. Už často překonává UTF-8, ale stále je co zlepšovat.

Co je horší? Za prvé, máme podmínku, a to aktuální posun abecedy a zaškrtávací políčko dlouhý režim. To nás dále omezuje: nyní mohou být stejné znaky kódovány odlišně v různých kontextech. Například hledání podřetězců bude muset být provedeno s ohledem na toto, a nikoli pouze porovnáním bajtů. Za druhé, jakmile jsme změnili abecedu, zhoršilo se to s kódováním ASCII znaků (a to není jen latinka, ale i základní interpunkce včetně mezer) - vyžadují změnu abecedy znovu na 0, tzn. opět jeden bajt navíc (a pak další, abychom se vrátili k našemu hlavnímu bodu).

Jedna abeceda je dobrá, dvě jsou lepší

Zkusme trochu změnit naše bitové předpony a vmáčknout ještě jednu ke třem popsaným výše:

0xxxxxxx — 1 byte v normálním režimu, 2 v dlouhém režimu
11xxxxxx — 1 bajt
100xxxxx xxxxxxxx - 2 bajty
101xxxxx xxxxxxxx xxxxxxxx - 3 bajty

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8

Nyní je ve dvoubajtovém záznamu o jeden dostupný bit méně - kód ukazuje až 0x1FFFA ne 0x3FFF. Stále je však znatelně větší než u dvoubajtových kódů UTF-8, většina běžných jazyků se stále vejde, nejviditelnější ztráta vypadla hiragana и katakana, Japonci jsou smutní.

Co je to za nový kód? 11xxxxxx? Toto je malá „skrýš“ o velikosti 64 znaků, která doplňuje naši hlavní abecedu, takže jsem ji nazval pomocnou (pomocný) abeceda. Když přepneme aktuální abecedu, část staré abecedy se stane pomocnou. Například jsme přešli z ASCII na azbuku - skrýš nyní obsahuje 64 znaků Latinská abeceda, čísla, mezera a čárka (nejčastější vkládání do neASCII textů). Přepněte zpět na ASCII - a hlavní část azbuky se stane pomocnou abecedou.

Díky přístupu ke dvěma abecedám zvládneme velké množství textů s minimálními náklady na přepínání abecedy (interpunkce nejčastěji povede k návratu k ASCII, ale poté získáme mnoho neASCII znaků z doplňkové abecedy, aniž bychom znovu přepnout).

Bonus: předpona dílčí abecedy 11xxxxxx a výběr jeho počátečního offsetu být 0xC0, získáme částečnou kompatibilitu s CP1252. Jinými slovy, mnoho (ale ne všechny) západoevropské texty zakódované v CP1252 budou vypadat stejně v UTF-C.

Zde však nastává potíž: jak získat pomocnou z hlavní abecedy? Můžete ponechat stejný offset, ale – bohužel – zde už hraje struktura Unicode proti nám. Velmi často není hlavní část abecedy na začátku bloku (například ruské velké „A“ má kód 0x0410, ačkoliv blok azbuky začíná 0x0400). Když tedy vezmeme prvních 64 znaků do skrýše, můžeme ztratit přístup k zadní části abecedy.

Abych tento problém vyřešil, ručně jsem prošel některé bloky odpovídající různým jazykům a určil jsem pro ně offset pomocné abecedy v rámci hlavní. Latinská abeceda byla jako výjimka obecně přeuspořádána jako základ64.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8

Závěrečné doteky

Pojďme se konečně zamyslet nad tím, kde ještě můžeme něco zlepšit.

Všimněte si, že formát 101xxxxx xxxxxxxx xxxxxxxx umožňuje kódovat čísla až 0x1FFFFFa Unicode končí dříve, v 0x10FFFF. Jinými slovy, poslední bod kódu bude reprezentován jako 10110000 11111111 11111111. Můžeme tedy říci, že pokud je první bajt ve tvaru 1011xxxx (Kde xxxx větší než 0), pak to znamená něco jiného. Můžete tam například přidat dalších 15 znaků, které jsou neustále k dispozici pro kódování v jednom bajtu, ale rozhodl jsem se to udělat jinak.

Podívejme se nyní na ty bloky Unicode, které vyžadují tři bajty. V podstatě, jak již bylo zmíněno, jsou to čínské znaky - ale je těžké s nimi něco dělat, je jich 21 tisíc. Ale létala tam i hiragana a katakana - a už jich není tolik, necelé dvě stovky. A protože jsme si vzpomněli na Japonce, nechybí ani emotikony (ve skutečnosti jsou v Unicode rozesety na mnoha místech, ale hlavní bloky jsou v dosahu 0x1F300 - 0x1FBFF). Pokud se zamyslíte nad tím, že nyní existují emotikony, které se skládají z několika bodů kódu najednou (například emodži ‍‍‍Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8 sestává až ze 7 kódů!), pak je úplná škoda utratit za každý tři bajty (7×3 = 21 bajtů kvůli jedné ikoně, noční můra).

Vybereme proto několik vybraných rozsahů odpovídajících emoji, hiraganě a katakaně, přečíslujeme je do jednoho souvislého seznamu a zakódujeme jako dva bajty místo tří:

1011xxxx xxxxxxxx

Skvělé: výše uvedené emotikonyDalší kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8, skládající se ze 7 kódových bodů, zabírá 8 bajtů v UTF-25 a vejdeme se do něj 14 (přesně dva bajty pro každý kódový bod). Mimochodem Habr to odmítl strávit (ve starém i v novém editoru), takže jsem to musel vložit s obrázkem.

Zkusme vyřešit ještě jeden problém. Jak si pamatujeme, základní abeceda je v podstatě vysoká 6 bitů, který máme na paměti a lepíme do kódu každého dalšího dekódovaného symbolu. V případě čínských znaků, které jsou v bloku 0x4E00 - 0x9FFF, je to buď bit 0 nebo 1. To není příliš pohodlné: budeme muset neustále přepínat abecedu mezi těmito dvěma hodnotami (tj. utratit tři bajty). Všimněte si však, že v dlouhém režimu můžeme od samotného kódu odečíst počet znaků, které kódujeme pomocí krátkého režimu (po všech výše popsaných tricích je to 10240 XNUMX) - pak se rozsah hieroglyfů posune na 0x2600 - 0x77FFa v tomto případě se v celém tomto rozsahu bude nejvýznamnějších 6 bitů (z 21) rovnat 0. Sekvence hieroglyfů tedy budou používat dva bajty na hieroglyf (což je optimální pro tak velký rozsah), aniž by způsobující přepínání abecedy.

Alternativní řešení: SCSU, BOCU-1

Odborníci na Unicode, kteří si právě přečetli název článku, vám s největší pravděpodobností připomenou, že přímo mezi standardy Unicode existuje Standardní kompresní schéma pro Unicode (SCSU), který popisuje metodu kódování velmi podobnou metodě popsané v článku.

Přiznám se upřímně: o jeho existenci jsem se dozvěděl až poté, co jsem byl hluboce ponořen do psaní svého rozhodnutí. Kdybych o tom věděl od začátku, pravděpodobně bych místo vymýšlení vlastního přístupu zkusil napsat implementaci.

Zajímavé je, že SCSU používá nápady velmi podobné těm, na které jsem přišel sám (místo pojmu „abecedy“ používají „okna“ a je jich k dispozici více, než mám já). Zároveň má tento formát i nevýhody: je o něco blíže kompresním algoritmům než kódovacím. Norma zejména poskytuje mnoho reprezentačních metod, ale neříká, jak vybrat tu optimální - k tomu musí kodér použít nějaký druh heuristiky. Kodér SCSU, který vytváří dobré balení, bude tedy složitější a těžkopádnější než můj algoritmus.

Pro srovnání jsem do JavaScriptu přenesl poměrně jednoduchou implementaci SCSU - co do objemu kódu mi vyšel srovnatelně s mým UTF-C, ale v některých případech byl výsledek o desítky procent horší (někdy to může přesáhnout, ale moc ne). Například texty v hebrejštině a řečtině byly kódovány UTF-C O 60 % lepší než SCSU (pravděpodobně kvůli jejich kompaktním abecedám).

Samostatně dodám, že kromě SCSU existuje také další způsob, jak kompaktně reprezentovat Unicode - BOCU-1, ale cílí na kompatibilitu s MIME (což jsem nepotřeboval) a ke kódování zaujímá trochu jiný přístup. Nehodnotil jsem jeho účinnost, ale zdá se mi, že pravděpodobně nebude vyšší než SCSU.

Možná vylepšení

Algoritmus, který jsem představil, není svou konstrukcí univerzální (v tom se mé cíle pravděpodobně nejvíce rozcházejí s cíli Unicode Consortium). Již jsem zmínil, že byl vyvinut primárně pro jeden úkol (uložení vícejazyčného slovníku do stromu prefixů) a některé jeho funkce se nemusí dobře hodit pro jiné úkoly. Ale skutečnost, že to není standard, může být plus - můžete jej snadno upravit tak, aby vyhovoval vašim potřebám.

Například zřejmým způsobem se můžete zbavit přítomnosti stavu, vytvořit bezstavové kódování – prostě neaktualizujte proměnné offs, auxOffs и is21Bit v kodéru a dekodéru. V tomto případě nebude možné efektivně sbalit sekvence znaků stejné abecedy, ale bude zaručeno, že stejný znak bude vždy zakódován stejnými bajty, bez ohledu na kontext.

Kromě toho můžete kodér přizpůsobit konkrétnímu jazyku změnou výchozího stavu - například zaměřením na ruské texty nastavte kodér a dekodér na začátek offs = 0x0400 и auxOffs = 0. To dává smysl zejména v případě bezstavového režimu. Obecně to bude podobné použití starého osmibitového kódování, ale bez odebrání možnosti vkládat znaky ze všech Unicode podle potřeby.

Další nevýhodou zmíněnou dříve je, že ve velkém textu kódovaném v UTF-C neexistuje žádný rychlý způsob, jak najít hranici znaků nejblíže libovolnému bajtu. Pokud odříznete posledních řekněme 100 bajtů z kódované vyrovnávací paměti, riskujete, že dostanete odpadky, se kterými nemůžete nic dělat. Kódování není určeno pro ukládání multigigabajtových protokolů, ale obecně to lze opravit. Byte 0xBF se nikdy nesmí objevit jako první bajt (ale může být druhý nebo třetí). Proto při kódování můžete vložit sekvenci 0xBF 0xBF 0xBF každých řekněme 10 KB - pak, pokud potřebujete najít hranici, bude stačit skenovat vybraný kus, dokud se nenajde podobný marker. Po posledním 0xBF je zaručeně začátek postavy. (Při dekódování je třeba tuto sekvenci tří bajtů samozřejmě ignorovat.)

Sčítání

Pokud jste dočetli až sem, gratulujeme! Doufám, že jste se stejně jako já dozvěděli něco nového (nebo si osvěžili paměť) o struktuře Unicode.

Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8
Ukázková stránka. Příklad hebrejštiny ukazuje výhody oproti UTF-8 i SCSU.

Výše popsaný výzkum by neměl být považován za zásah do norem. S výsledky své práce jsem však vesměs spokojený, takže mám z nich radost sdílet: například minifikovaná knihovna JS váží pouze 1710 bajtů (a samozřejmě nemá žádné závislosti). Jak jsem již uvedl výše, její práce najdete na ukázková stránka (existuje i sada textů, na kterých to lze porovnat s UTF-8 a SCSU).

Nakonec ještě jednou upozorním na případy, kdy se používá UTF-C nestojí za to:

  • Pokud jsou vaše řádky dostatečně dlouhé (100–200 znaků). V tomto případě byste měli přemýšlet o použití kompresních algoritmů, jako je deflate.
  • Pokud potřebuješ ASCII průhlednost, to znamená, že je pro vás důležité, aby kódované sekvence neobsahovaly ASCII kódy, které nebyly v původním řetězci. Tomuto požadavku se lze vyhnout, pokud při interakci s rozhraními API třetích stran (například při práci s databází) předáte výsledek kódování jako abstraktní sadu bajtů, nikoli jako řetězce. V opačném případě riskujete neočekávaná zranitelnost.
  • Pokud chcete být schopni rychle najít hranice znaků s libovolným posunem (například když je poškozena část řádku). To lze provést, ale pouze skenováním řádku od začátku (nebo použitím úpravy popsané v předchozí části).
  • Pokud potřebujete rychle provádět operace s obsahem řetězců (třídit je, hledat v nich podřetězce, zřetězit). To vyžaduje, aby byly řetězce nejprve dekódovány, takže UTF-C bude v těchto případech pomalejší než UTF-8 (ale rychlejší než kompresní algoritmy). Vzhledem k tomu, že stejný řetězec je vždy zakódován stejným způsobem, není vyžadováno přesné porovnání dekódování a lze jej provést po bytech.

aktualizace: uživatel Tyomitch v komentářích níže zveřejnil graf zdůrazňující limity použitelnosti UTF-C. Ukazuje, že UTF-C je efektivnější než obecný kompresní algoritmus (variace LZW), pokud je zabalený řetězec kratší. ~140 znaků (podotýkám však, že srovnání bylo provedeno na jednom textu, pro jiné jazyky se může výsledek lišit).
Další kolo: struny Unicode skladujeme o 30-60 % kompaktnější než UTF-8

Zdroj: www.habr.com

Přidat komentář