Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8

Če ste razvijalec in se soočate z nalogo izbire kodiranja, bo Unicode skoraj vedno prava rešitev. Specifični način predstavitve je odvisen od konteksta, vendar je najpogosteje tudi tukaj univerzalni odgovor - UTF-8. Dobra stvar pri njem je, da vam omogoča uporabo vseh znakov Unicode brez porabe preveč v večini primerov veliko bajtov. Res je, da za jezike, ki uporabljajo več kot le latinico, "ne preveč" vsaj dva bajta na znak. Ali lahko storimo bolje, ne da bi se vrnili k prazgodovinskim kodiranjem, ki nas omejujejo na samo 256 razpoložljivih znakov?

Spodaj predlagam, da se seznanite s svojim poskusom odgovora na to vprašanje in implementirate relativno preprost algoritem, ki vam omogoča shranjevanje vrstic v večini jezikov sveta, ne da bi dodali odvečnost, ki je v UTF-8.

Zavrnitev odgovornosti. Takoj bom podal nekaj pomembnih pridržkov: opisana rešitev ni na voljo kot univerzalna zamenjava za UTF-8, je primeren le v ozkem seznamu primerov (več o njih spodaj) in v nobenem primeru se ne sme uporabljati za interakcijo z API-ji tretjih oseb (ki za to sploh ne vedo). Najpogosteje so splošni algoritmi stiskanja (na primer deflate) primerni za kompaktno shranjevanje velikih količin besedilnih podatkov. Poleg tega sem že med ustvarjanjem svoje rešitve našel obstoječi standard v samem Unicodeu, ki rešuje isto težavo - je nekoliko bolj zapleten (in pogosto slabši), vendar je še vedno sprejet standard in ne samo postavljen skupaj na kolenu. Povedal vam bom tudi o njem.

O Unicode in UTF-8

Za začetek nekaj besed o tem, kaj je Unicode и UTF-8.

Kot veste, je bilo nekoč priljubljeno 8-bitno kodiranje. Pri njih je bilo vse preprosto: 256 znakov je mogoče oštevilčiti s številkami od 0 do 255, številke od 0 do 255 pa je očitno mogoče predstaviti kot en bajt. Če se vrnemo na sam začetek, je kodiranje ASCII povsem omejeno na 7 bitov, torej je najpomembnejši bit v njegovi bajtni predstavitvi nič, večina 8-bitnih kodiranj pa je združljivih z njim (razlikujejo se le v “zgornjem” del, kjer je najpomembnejši bit ena ).

Kako se Unicode razlikuje od teh kodiranj in zakaj je z njim povezanih toliko specifičnih predstavitev – UTF-8, UTF-16 (BE in LE), UTF-32? Razvrstimo po vrsti.

Osnovni standard Unicode opisuje samo ujemanje med znaki (in v nekaterih primerih posameznimi komponentami znakov) in njihovimi številkami. In v tem standardu je veliko možnih številk - od 0x00 za 0x10FFFF (1 kosov). Če bi želeli v spremenljivko vnesti število v takem obsegu, nam ne bi zadostovala ne 114 ne 112 bajta. In ker naši procesorji niso ravno zasnovani za delo s tribajtnimi številkami, bi bili prisiljeni uporabiti kar 1 bajte na znak! To je UTF-2, vendar ravno zaradi te "potratnosti" ta oblika ni priljubljena.

Na srečo vrstni red znakov v Unicode ni naključen. Njihov celoten sklop je razdeljen na 17 "letala", od katerih vsaka vsebuje 65536 (0x10000) «kodne točke" Koncept "kodne točke" je tukaj preprost številka znaka, ki mu ga je dodelil Unicode. Toda, kot je navedeno zgoraj, v Unicode niso oštevilčeni le posamezni znaki, temveč tudi njihove komponente in storitvene oznake (včasih pa številki sploh nič ne ustreza - morda zaenkrat, vendar za nas to ni tako pomembno), tako da pravilneje je vedno govoriti posebej o številu samih številk in ne o simbolih. Vendar bom v nadaljevanju zaradi jedrnatosti pogosto uporabljal besedo "simbol", kar pomeni izraz "kodna točka".

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Unicode letala. Kot lahko vidite, je večina (letala 4 do 13) še neuporabljenih.

Najbolj presenetljivo je, da vsa glavna "pulpa" leži v ničelni ravnini, imenuje se "Osnovno večjezično letalo". Če vrstica vsebuje besedilo v enem od sodobnih jezikov (vključno s kitajščino), ne boste presegli te ravnine. Vendar tudi ne morete odrezati preostalega Unicode - na primer, emoji se večinoma nahajajo na koncu naslednje letalo,«Dodatno večjezično letalo« (razteza se od 0x10000 za 0x1FFFF). Torej UTF-16 naredi to: vsi znaki, ki spadajo znotraj Osnovno večjezično letalo, so kodirani "kot so" z ustrezno dvobajtno številko. Vendar nekatere številke v tem obsegu sploh ne označujejo določenih znakov, temveč kažejo, da moramo za tem parom bajtov upoštevati še enega - s kombiniranjem vrednosti teh štirih bajtov skupaj dobimo število, ki pokriva celotno veljavno območje Unicode. Ta ideja se imenuje "nadomestni pari" - morda ste že slišali zanje.

Torej UTF-16 zahteva dva ali (v zelo redkih primerih) štiri bajte na "kodno točko". To je bolje kot uporaba štirih bajtov ves čas, vendar latinica (in drugi znaki ASCII), če so kodirani na ta način, porabijo polovico prostora za ničle. UTF-8 je zasnovan tako, da to popravi: ASCII v njem zaseda, kot prej, samo en bajt; kode iz 0x80 za 0x7FF - dva bajta; od 0x800 za 0xFFFF - tri in od 0x10000 za 0x10FFFF - štiri. Po eni strani je latinica postala dobra: združljivost z ASCII se je vrnila, porazdelitev pa je bolj enakomerno "razporejena" od 1 do 4 bajtov. Toda abecede, razen latinice, žal nimajo nobene koristi v primerjavi z UTF-16 in mnoge zdaj potrebujejo tri bajte namesto dveh - obseg, ki ga pokriva dvobajtni zapis, se je zožil za 32-krat, z 0xFFFF za 0x7FF, vanj pa ni vključena ne kitajščina ne na primer gruzijska. Cirilica in pet drugih abeced - hura - sreča, 2 bajta na znak.

Zakaj se to zgodi? Poglejmo, kako UTF-8 predstavlja kode znakov:
Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Neposredno za predstavitev števil se tukaj uporabljajo biti, označeni s simbolom x. Vidimo, da je v dvobajtnem zapisu le 11 takih bitov (od 16). Vodilni bitovi imajo tukaj le pomožno funkcijo. V primeru štiribajtnega zapisa je 21 od 32 bitov dodeljenih za številko kodne točke - zdi se, da bi bili trije bajti (ki dajejo skupaj 24 bitov) dovolj, vendar servisni markerji požrejo preveč.

Je to slabo? res ne. Po eni strani, če nam je zelo mar za prostor, imamo algoritme za stiskanje, ki zlahka odpravijo vso dodatno entropijo in redundanco. Po drugi strani pa je bil cilj Unicode zagotoviti čim bolj univerzalno kodiranje. Na primer, vrstico, kodirano v UTF-8, lahko zaupamo kodi, ki je prej delovala samo z ASCII, in se ne bojimo, da bo videla znak iz območja ASCII, ki ga dejansko ni (navsezadnje v UTF-8 vse bajtov, ki se začnejo z ničelnim bitom - to je natanko tisto, kar je ASCII). In če nenadoma želimo odrezati majhen rep velikega niza, ne da bi ga dekodirali od samega začetka (ali obnoviti del informacij po poškodovanem odseku), zlahka najdemo odmik, kjer se znak začne (dovolj je za preskok bajtov, ki imajo predpono bit 10).

Zakaj bi potem izumljali nekaj novega?

Hkrati se občasno pojavijo situacije, ko so algoritmi stiskanja, kot je deflate, slabo uporabni, vendar želite doseči kompaktno shranjevanje nizov. Osebno sem naletel na to težavo, ko sem razmišljal o gradnji stisnjeno drevo predpon za velik slovar, ki vključuje besede v poljubnih jezikih. Po eni strani je vsaka beseda zelo kratka, zato bo njeno stiskanje neučinkovito. Po drugi strani je bila implementacija drevesa, ki sem jo obravnaval, zasnovana tako, da je vsak bajt shranjenega niza ustvaril ločeno vozlišče drevesa, zato je bilo zmanjševanje njihovega števila zelo koristno. V moji knjižnici Az.js (Kot v pimorfija2, na katerem temelji) je podoben problem mogoče rešiti preprosto - nizi zapakirani v DAWG-slovar, tam shranjen v dobri stari CP1251. Toda, kot je lahko razumeti, to deluje dobro le za omejeno abecedo - vrstice v kitajščini ni mogoče dodati v tak slovar.

Ločeno bi rad opozoril na še en neprijeten odtenek, ki se pojavi pri uporabi UTF-8 v takšni strukturi podatkov. Zgornja slika prikazuje, da ko je znak zapisan kot dva bajta, biti, povezani z njegovo številko, ne pridejo v vrsto, ampak so ločeni s parom bitov 10 v sredini: 110xxxxx 10xxxxxx. Zaradi tega, ko spodnjih 6 bitov drugega bajta preseže kodo znakov (tj. pride do prehoda 1011111110000000), potem se spremeni tudi prvi bajt. Izkazalo se je, da je črka "p" označena z bajti 0xD0 0xBF, in naslednji "r" je že 0xD1 0x80. V drevesu predpone to vodi do razdelitve nadrejenega vozlišča na dvoje - eno za predpono 0xD0, in drugo za 0xD1 (čeprav bi celotno cirilico lahko kodirali le z drugim bajtom).

Kaj sem dobil

Ko sem se soočil s to težavo, sem se odločil, da bom vadil igranje iger z biti, hkrati pa se malo bolje seznanil s strukturo Unicode kot celote. Rezultat je bil format kodiranja UTF-C ("C" za kompaktna), ki ne porabi več kot 3 bajte na kodno točko in zelo pogosto omogoča, da porabite samo en dodatni bajt za celotno kodirano vrstico. To vodi k dejstvu, da se na mnogih abecedah, ki niso ASCII, izkaže, da je takšno kodiranje 30-60 % bolj kompakten kot UTF-8.

Predstavil sem primere implementacije algoritmov za kodiranje in dekodiranje v obliki Knjižnici JavaScript in Go, jih lahko prosto uporabite v svoji kodi. Vendar bom vseeno poudaril, da ta format v nekem smislu ostaja "kolo" in ga ne priporočam ne da bi se zavedal, zakaj ga potrebuješ. To je še vedno bolj eksperiment kot resna "izboljšava UTF-8". Kljub temu je koda tam napisana lepo, jedrnato, z velikim številom komentarjev in pokritostjo testa.

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Rezultati testa in primerjava z UTF-8

Tudi jaz sem demo stran, kjer lahko ocenite delovanje algoritma, nato pa vam bom povedal več o njegovih načelih in razvojnem procesu.

Odprava odvečnih bitov

Za osnovo sem seveda vzel UTF-8. Prva in najbolj očitna stvar, ki jo lahko spremenimo v njem, je zmanjšanje števila servisnih bitov v vsakem bajtu. Na primer, prvi bajt v UTF-8 se vedno začne z bodisi 0ali z 11 - predpono 10 Imajo ga samo naslednji bajti. Zamenjajmo predpono 11 o 1, za naslednje bajte pa bomo popolnoma odstranili predpone. Kaj se bo zgodilo?

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

Čakaj, kje je štiribajtni zapis? A ni več potreben – pri pisanju v treh bajtih imamo zdaj na voljo 21 bitov in to zadostuje za vsa števila do 0x10FFFF.

Kaj smo žrtvovali tukaj? Najpomembnejše je zaznavanje meja znakov s poljubne lokacije v medpomnilniku. Ne moremo pokazati na poljuben bajt in iz njega najti začetka naslednjega znaka. To je omejitev našega formata, vendar je v praksi le redko potrebno. Običajno lahko tečemo skozi medpomnilnik od samega začetka (še posebej, ko gre za kratke vrstice).

Izboljšala se je tudi situacija s pokrivanjem jezikov z 2 bajtoma: zdaj dvobajtni format daje obseg 14 bitov, to pa so kode do 0x3FFF. Kitajci nimajo sreče (njihovi značaji se večinoma gibljejo od 0x4E00 za 0x9FFF), toda Gruzijci in številna druga ljudstva se bolj zabavajo - tudi njihovi jeziki se prilegajo 2 bajtom na znak.

Vnesite stanje kodirnika

Razmislimo zdaj o lastnostih samih črt. Slovar najpogosteje vsebuje besede, zapisane z znaki iste abecede, kar velja tudi za mnoga druga besedila. Dobro bi bilo, da to abecedo navedete enkrat, nato pa samo številko črke v njej. Poglejmo, ali nam bo razporeditev znakov v tabeli Unicode pomagala.

Kot je navedeno zgoraj, je Unicode razdeljen na letalo Vsak po 65536 kod. A to ni zelo uporabna delitev (kot že rečeno, največkrat smo v ničelni ravnini). Bolj zanimiva je delitev po bloki. Ti obsegi nimajo več fiksne dolžine in so bolj smiselni - praviloma vsak združuje znake iz iste abecede.

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Blok, ki vsebuje znake bengalske abecede. Na žalost je to zaradi zgodovinskih razlogov primer ne zelo goste embalaže - 96 znakov je kaotično razpršenih po 128 blokovnih kodnih točkah.

Začetki blokov in njihove velikosti so vedno večkratniki 16 - to se naredi preprosto zaradi udobja. Poleg tega se številni bloki začnejo in končajo z vrednostmi, ki so večkratniki 128 ali celo 256 - na primer, osnovna cirilica zavzame 256 bajtov od 0x0400 za 0x04FF. To je zelo priročno: če enkrat shranimo predpono 0x04, potem lahko kateri koli znak v cirilici zapišemo v en bajt. Res je, na ta način bomo izgubili možnost, da se vrnemo na ASCII (in na vse druge znake na splošno). Zato naredimo tole:

  1. Dva bajta 10yyyyyy yxxxxxxx ne označujejo le simbola s številko yyyyyy yxxxxxxx, ampak tudi spremeniti trenutna abeceda o yyyyyy y0000000 (tj. spomnimo se vseh bitov razen najmanj pomembnih 7 bit);
  2. En bajt 0xxxxxxx to je znak trenutne abecede. Dodati ga je treba le odmiku, ki smo si ga zapomnili v 1. koraku. Čeprav nismo spremenili abecede, je odmik enak nič, zato smo ohranili združljivost z ASCII.

Podobno za kode, ki zahtevajo 3 bajte:

  1. Trije bajti 110yyyyy yxxxxxxx xxxxxxxx označite simbol s številko yyyyyy yxxxxxxx xxxxxxxx, spremeniti trenutna abeceda o yyyyyy y0000000 00000000 (spomnil sem se vsega razen mlajših 15 bit) in potrdite polje, v katerem smo zdaj dolga način (ko spremenimo abecedo nazaj na dvobajtno, bomo to zastavico ponastavili);
  2. Dva bajta 0xxxxxxx xxxxxxxx v dolgem načinu je to znak trenutne abecede. Podobno ga dodamo z odmikom iz koraka 1. Edina razlika je, da zdaj beremo dva bajta (ker smo preklopili na ta način).

Sliši se dobro: zdaj, ko moramo kodirati znake iz istega 7-bitnega območja Unicode, porabimo 1 dodaten bajt na začetku in skupno en bajt na znak.

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Delo iz ene od prejšnjih različic. Že zdaj pogosto premaga UTF-8, vendar je še vedno prostor za izboljšave.

Kaj je huje? Prvič, imamo pogoj, namreč odmik trenutne abecede in potrditveno polje dolg način. To nas dodatno omejuje: zdaj se lahko isti znaki različno kodirajo v različnih kontekstih. Iskanje podnizov, na primer, bo moralo potekati ob upoštevanju tega, ne le s primerjavo bajtov. Drugič, takoj ko smo spremenili abecedo, je postalo slabo s kodiranjem znakov ASCII (in to ni samo latinica, ampak tudi osnovna ločila, vključno s presledki) - zahtevajo ponovno spremembo abecede na 0, tj. spet dodaten bajt (in nato še enega, da se vrnemo k glavni točki).

Ena abeceda je dobra, dve sta boljši

Poskusimo malo spremeniti naše bitne predpone, tako da k zgoraj opisanim trem dodamo še eno:

0xxxxxxx — 1 bajt v običajnem načinu, 2 v dolgem načinu
11xxxxxx — 1 bajt
100xxxxx xxxxxxxx - 2 bajta
101xxxxx xxxxxxxx xxxxxxxx - 3 bajta

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8

Zdaj je v dvobajtnem zapisu en razpoložljiv bit manj - kodne točke do 0x1FFFIn ne 0x3FFF. Vendar je še vedno opazno večji kot pri dvobajtnih kodah UTF-8, večina običajnih jezikov še vedno ustreza, najbolj opazna izguba je izpadla hiragana и katakana, Japonci so žalostni.

Kaj je ta nova koda? 11xxxxxx? To je majhen "sklad" velikosti 64 znakov, dopolnjuje našo glavno abecedo, zato sem jo imenoval pomožna (pomožni) abeceda. Ko zamenjamo trenutno abecedo, del stare abecede postane pomožna. Na primer, preklopili smo z ASCII na cirilico - shramba zdaj vsebuje 64 znakov, ki vsebujejo Latinska abeceda, številke, presledek in vejica (najpogostejši vstavitve v ne-ASCII besedila). Preklopite nazaj na ASCII - in glavni del cirilice bo postal pomožna abeceda.

Zahvaljujoč dostopu do dveh abeced lahko obdelamo veliko število besedil z minimalnimi stroški menjave abecede (ločila bodo najpogosteje privedla do vrnitve v ASCII, potem pa bomo dobili veliko ne-ASCII znakov iz dodatne abecede, brez ponovno preklapljanje).

Bonus: predpona podabecede 11xxxxxx in izbiro njegovega začetnega odmika 0xC0, dobimo delno združljivost s CP1252. Z drugimi besedami, veliko (vendar ne vsa) zahodnoevropska besedila, kodirana v CP1252, bodo videti enaka v UTF-C.

Tu pa se pojavi težava: kako iz glavne abecede pridobiti pomožno? Lahko pustite enak odmik, toda - žal - tukaj struktura Unicode že igra proti nam. Zelo pogosto glavni del abecede ni na začetku bloka (na primer ruska velika črka "A" ima kodo 0x0410, čeprav se blok cirilice začne z 0x0400). Tako lahko izgubimo dostop do zadnjega dela abecede, ko vzamemo prvih 64 znakov v shrambo.

Da bi rešil to težavo, sem ročno pregledal nekaj blokov, ki ustrezajo različnim jezikom, in določil odmik pomožne abecede znotraj glavne zanje. Latinska abeceda je bila kot izjema na splošno preurejena kot base64.

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8

Zadnji dotiki

Končno razmislimo, kje bi še lahko kaj izboljšali.

Upoštevajte, da oblika 101xxxxx xxxxxxxx xxxxxxxx omogoča kodiranje številk do 0x1FFFFF, Unicode pa se konča prej, pri 0x10FFFF. Z drugimi besedami, zadnja kodna točka bo predstavljena kot 10110000 11111111 11111111. Zato lahko rečemo, da če je prvi bajt oblike 1011xxxx (Kje xxxx večje od 0), potem pomeni nekaj drugega. Na primer, tam lahko dodate še 15 znakov, ki so stalno na voljo za kodiranje v enem bajtu, vendar sem se odločil narediti drugače.

Poglejmo zdaj tiste bloke Unicode, ki zahtevajo tri bajte. V bistvu, kot že omenjeno, so to kitajski znaki - vendar je z njimi težko kar koli narediti, 21 tisoč jih je. Toda tja sta leteli tudi hiragana in katakana - in teh ni več toliko, manj kot dvesto. In, ker smo se spomnili Japoncev, obstajajo tudi emojiji (pravzaprav so razpršeni na številnih mestih v Unicode, vendar so glavni bloki v območju 0x1F300 - 0x1FBFF). Če pomislite na dejstvo, da zdaj obstajajo emojiji, ki so sestavljeni iz več kodnih točk hkrati (na primer emoji ‍‍‍Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8 sestoji iz kar 7 kod!), potem postane čista škoda porabiti tri bajte za vsako (7×3 = 21 bajtov za eno ikono, nočna mora).

Zato izberemo nekaj izbranih obsegov, ki ustrezajo emojijem, hiragani in katakani, jih preštevilčimo v en neprekinjen seznam in kodiramo kot dva bajta namesto treh:

1011xxxx xxxxxxxx

Odlično: zgoraj omenjeni emojiŠe eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8, ki je sestavljen iz 7 kodnih točk, zavzema 8 bajtov v UTF-25 in ga umestimo v 14 (natančno dva bajta za vsako kodno točko). Mimogrede, Habr ga ni hotel prebaviti (tako v starem kot v novem urejevalniku), zato sem ga moral vstaviti s sliko.

Poskusimo odpraviti še eno težavo. Kot se spomnimo, je osnovna abeceda v bistvu visokih 6 bitov, ki si ga zapomnimo in ga prilepimo na kodo vsakega naslednjega dekodiranega simbola. V primeru kitajskih znakov, ki so v bloku 0x4E00 - 0x9FFF, to je bodisi bit 0 ali 1. To ni zelo priročno: morali bomo nenehno preklapljati abecedo med tema dvema vrednostma (tj. porabiti tri bajte). Upoštevajte pa, da lahko v dolgem načinu od same kode odštejemo število znakov, ki jih kodiramo s kratkim načinom (po vseh zgoraj opisanih trikih je to 10240) - takrat se obseg hieroglifov premakne na 0x2600 - 0x77FF, in v tem primeru bo v tem celotnem obsegu najpomembnejših 6 bitov (od 21) enakih 0. Tako bodo zaporedja hieroglifov uporabljala dva bajta na hieroglif (kar je optimalno za tako velik obseg), brez povzroča zamenjavo abecede.

Alternativne rešitve: SCSU, BOCU-1

Strokovnjaki za Unicode, ki so pravkar prebrali naslov članka, vas bodo najverjetneje pohiteli spomniti, da je neposredno med standardi Unicode Standardna shema stiskanja za Unicode (SCSU), ki opisuje metodo kodiranja, ki je zelo podobna tisti, ki je opisana v članku.

Iskreno priznam: za njegov obstoj sem izvedel šele, ko sem se globoko poglobil v pisanje svoje odločitve. Če bi vedel za to od začetka, bi verjetno poskusil napisati implementacijo, namesto da bi se domislil svojega pristopa.

Zanimivo je, da SCSU uporablja ideje, ki so zelo podobne tistim, ki sem si jih sam izmislil (namesto pojma “abecede” uporabljajo “okna” in teh je na voljo več kot jaz). Hkrati pa ima ta format tudi slabosti: je nekoliko bližje algoritmom stiskanja kot kodirnim. Zlasti standard podaja številne predstavitvene metode, vendar ne pove, kako izbrati optimalno - za to mora kodirnik uporabiti neke vrste hevristiko. Tako bo kodirnik SCSU, ki ustvari dobro embalažo, bolj zapleten in bolj okoren kot moj algoritem.

Za primerjavo sem relativno preprosto implementacijo SCSU prenesel v JavaScript - po obsegu kode se je izkazalo za primerljivo z mojim UTF-C, vendar je bil v nekaterih primerih rezultat za več deset odstotkov slabši (včasih ga lahko preseže, vendar ne veliko). Na primer, besedila v hebrejščini in grščini so bila kodirana z UTF-C 60 % boljši od SCSU (verjetno zaradi njihove kompaktne abecede).

Ločeno bom dodal, da poleg SCSU obstaja še en način za kompaktno predstavitev Unicode - BOCU-1, vendar cilja na združljivost MIME (ki je nisem potreboval) in uporablja nekoliko drugačen pristop k kodiranju. Nisem ocenil njegove učinkovitosti, vendar se mi zdi, da je malo verjetno, da bo višja od SCSU.

Možne izboljšave

Algoritem, ki sem ga predstavil, po zasnovi ni univerzalen (tu se moji cilji verjetno najbolj razlikujejo od ciljev konzorcija Unicode). Omenil sem že, da je bil razvit predvsem za eno nalogo (shranjevanje večjezičnega slovarja v drevesu predpon) in nekatere njegove funkcije morda niso najbolj primerne za druge naloge. Toda dejstvo, da ni standard, je lahko plus - enostavno ga lahko spremenite tako, da ustreza vašim potrebam.

Na primer, na očiten način se lahko znebite prisotnosti stanja, naredite kodiranje brez stanja - samo ne posodabljajte spremenljivk offs, auxOffs и is21Bit v kodirniku in dekodirniku. V tem primeru ne bo mogoče učinkovito zapakirati zaporedij znakov iste abecede, vendar bo zagotovljeno, da bo isti znak vedno kodiran z istimi bajti, ne glede na kontekst.

Poleg tega lahko kodirnik prilagodite določenemu jeziku, tako da spremenite privzeto stanje - na primer, če se osredotočite na ruska besedila, nastavite kodirnik in dekoder na začetku offs = 0x0400 и auxOffs = 0. To je še posebej smiselno v primeru načina brez stanja. Na splošno bo to podobno uporabi starega osembitnega kodiranja, vendar brez odstranitve možnosti vstavljanja znakov iz celotnega Unicode po potrebi.

Druga prej omenjena pomanjkljivost je, da v velikem besedilu, kodiranem v UTF-C, ni hitrega načina za iskanje meje znakov, ki je najbližje poljubnemu bajtu. Če odrežete zadnjih, recimo 100 bajtov iz kodiranega medpomnilnika, tvegate, da dobite smeti, s katerimi ne morete narediti ničesar. Kodiranje ni namenjeno shranjevanju večgigabajtnih dnevnikov, vendar je na splošno to mogoče popraviti. Bajt 0xBF nikoli ne sme biti prikazan kot prvi bajt (lahko pa je drugi ali tretji). Zato lahko pri kodiranju vstavite zaporedje 0xBF 0xBF 0xBF vsakih, recimo, 10 KB - takrat, če morate najti mejo, bo dovolj, da skenirate izbrani kos, dokler ne najdete podobnega markerja. Po zadnjem 0xBF je zagotovljeno začetek lika. (Pri dekodiranju bo to zaporedje treh bajtov seveda treba prezreti.)

Če povzamemo:

Če ste prebrali tako daleč, čestitamo! Upam, da ste se, tako kot jaz, naučili kaj novega (ali osvežili spomin) o strukturi Unicode.

Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8
Demo stran. Primer hebrejščine kaže prednosti pred UTF-8 in SCSU.

Zgoraj opisane raziskave ne smemo obravnavati kot poseg v standarde. Sem pa na splošno z rezultati svojega dela zadovoljna, tako da sem z njimi zadovoljna deliti: na primer, pomanjšana knjižnica JS tehta samo 1710 bajtov (in nima odvisnosti, seveda). Kot sem omenil zgoraj, lahko njeno delo najdete na demo stran (obstaja tudi nabor besedil, na podlagi katerih se lahko primerja z UTF-8 in SCSU).

Na koncu bom še enkrat opozoril na primere uporabe UTF-C ni vredno:

  • Če so vaše vrstice dovolj dolge (od 100-200 znakov). V tem primeru bi morali razmisliti o uporabi algoritmov stiskanja, kot je deflate.
  • Če potrebujete Preglednost ASCII, to pomeni, da je za vas pomembno, da kodirana zaporedja ne vsebujejo kod ASCII, ki jih ni v izvirnem nizu. Potrebi po tem se je mogoče izogniti, če pri interakciji z API-ji tretjih oseb (na primer pri delu z bazo podatkov) posredujete rezultat kodiranja kot abstrakten niz bajtov in ne kot nize. V nasprotnem primeru tvegate, da boste dobili nepričakovane ranljivosti.
  • Če želite hitro najti meje znakov pri poljubnem odmiku (na primer, ko je del vrstice poškodovan). To je mogoče storiti, vendar samo s skeniranjem vrstice od začetka (ali z uporabo spremembe, opisane v prejšnjem razdelku).
  • Če morate hitro izvesti operacije z vsebino nizov (razvrstiti jih, poiskati podnize v njih, združiti). To zahteva, da se nizi najprej dekodirajo, zato bo UTF-C v teh primerih počasnejši od UTF-8 (vendar hitrejši od algoritmov za stiskanje). Ker je isti niz vedno kodiran na enak način, natančna primerjava dekodiranja ni potrebna in se lahko izvede na osnovi bajt za bajtom.

Posodobitev: Uporabnik Tyomitch v spodnjih komentarjih objavil graf, ki poudarja omejitve uporabnosti UTF-C. Kaže, da je UTF-C učinkovitejši od algoritma za stiskanje splošnega namena (različica LZW), dokler je pakirani niz krajši ~140 znakov (vendar ugotavljam, da je bila primerjava izvedena na enem besedilu; za druge jezike se lahko rezultat razlikuje).
Še eno kolo: nize Unicode shranjujemo 30–60 % bolj kompaktno kot UTF-8

Vir: www.habr.com

Dodaj komentar