Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8

Ako ste programer i suočeni ste sa zadatkom odabira kodiranja, tada će Unicode gotovo uvijek biti pravo rješenje. Konkretna metoda predstavljanja zavisi od konteksta, ali najčešće i ovde postoji univerzalni odgovor - UTF-8. Dobra stvar u vezi s tim je što vam omogućava da koristite sve Unicode znakove bez trošenja previše mnogo bajtova u većini slučajeva. Istina, za jezike koji koriste više od latinice, "ne previše" je barem dva bajta po karakteru. Možemo li učiniti bolje bez vraćanja na praistorijska kodiranja koja nas ograničavaju na samo 256 dostupnih znakova?

U nastavku predlažem da se upoznate sa svojim pokušajem da odgovorim na ovo pitanje i implementiram relativno jednostavan algoritam koji vam omogućava pohranjivanje linija na većini jezika svijeta bez dodavanja redundancije koja je u UTF-8.

Odricanje od odgovornosti. Odmah ću napraviti nekoliko važnih rezervi: opisano rješenje nije ponuđeno kao univerzalna zamjena za UTF-8, prikladan je samo u uskom popisu slučajeva (više o njima u nastavku), i ni u kojem slučaju ga ne bi trebalo koristiti za interakciju s API-jima trećih strana (koji čak i ne znaju za to). Najčešće su algoritmi kompresije opće namjene (na primjer, deflate) prikladni za kompaktno skladištenje velikih količina tekstualnih podataka. Osim toga, već u procesu kreiranja svog rješenja pronašao sam postojeći standard u samom Unicode-u, koji rješava isti problem - nešto je komplikovaniji (a često i gori), ali je ipak prihvaćen standard, a ne samo stavljen zajedno na kolenima. Reći ću ti i o njemu.

O Unicode-u i UTF-8

Za početak, nekoliko riječi o tome šta je to unicode и UTF-8.

Kao što znate, 8-bitna kodiranja su nekada bila popularna. Kod njih je sve bilo jednostavno: 256 karaktera može se numerisati brojevima od 0 do 255, a brojevi od 0 do 255 se očito mogu predstaviti kao jedan bajt. Ako se vratimo na sam početak, ASCII kodiranje je potpuno ograničeno na 7 bita, tako da je najznačajniji bit u njegovoj bajt reprezentaciji nula, a većina 8-bitnih kodiranja je kompatibilna s njim (razlikuju se samo u "gornjem" dio, gdje je najznačajniji bit jedan ).

Po čemu se Unicode razlikuje od tih kodiranja i zašto je toliko specifičnih reprezentacija povezanih s njim - UTF-8, UTF-16 (BE i LE), UTF-32? Hajde da to sredimo po redu.

Osnovni Unicode standard opisuje samo korespondenciju između znakova (iu nekim slučajevima, pojedinačnih komponenti znakova) i njihovih brojeva. I postoji mnogo mogućih brojeva u ovom standardu - od 0x00 do 0x10FFFF (1 komada). Ako želimo da u promenljivu stavimo broj u takvom opsegu, ne bi nam bili dovoljni ni 114 ni 112 bajta. A pošto naši procesori nisu baš dizajnirani za rad sa brojevima od tri bajta, bili bismo primorani da koristimo čak 1 bajta po karakteru! Ovo je UTF-2, ali upravo zbog te "rasipnosti" ovaj format nije popularan.

Na sreću, redosled znakova unutar Unicode-a nije slučajan. Njihov cijeli set je podijeljen na 17"avioni", od kojih svaki sadrži 65536 (0x10000) «kodne tačke" Koncept "kodne tačke" ovdje je jednostavan broj karaktera, koje mu je dodijelio Unicode. Ali, kao što je gore spomenuto, u Unicodeu nisu numerirani samo pojedinačni znakovi, već i njihove komponente i servisne oznake (a ponekad baš ništa ne odgovara broju - možda za sada, ali za nas to nije toliko važno), tako da ispravnije je uvijek govoriti konkretno o broju samih brojeva, a ne simbolima. Međutim, u nastavku ću, radi sažetosti, često koristiti riječ “simbol”, podrazumijevajući pojam “kodna tačka”.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Unicode avioni. Kao što vidite, većina (avioni 4 do 13) je još uvijek neiskorištena.

Ono što je najupečatljivije je da sva glavna "pulpa" leži u nultoj ravni, zove se "Osnovna višejezična ravan". Ako linija sadrži tekst na jednom od modernih jezika (uključujući kineski), nećete ići dalje od ove ravni. Ali ne možete odrezati ni ostatak Unicodea - na primjer, emoji se uglavnom nalaze na kraju sledeći avion"Dodatni višejezični avion"(proteže se od 0x10000 do 0x1FFFF). Dakle, UTF-16 radi ovo: svi znakovi spadaju unutar Osnovna višejezična ravan, su kodirani “kao što jesu” odgovarajućim dvobajtnim brojem. Međutim, neki od brojeva u ovom rasponu uopće ne označavaju određene znakove, već ukazuju na to da nakon ovog para bajtova moramo razmotriti još jedan - kombiniranjem vrijednosti ova četiri bajta zajedno, dobijamo broj koji pokriva cijeli važeći Unicode raspon. Ova ideja se zove "surogat parovi" - možda ste čuli za njih.

Dakle, UTF-16 zahtijeva dva ili (u vrlo rijetkim slučajevima) četiri bajta po "kodnoj tački". Ovo je bolje od korištenja četiri bajta cijelo vrijeme, ali latinica (i drugi ASCII znakovi) kada se kodiraju na ovaj način troše polovinu prostora na nule. UTF-8 je dizajniran da ovo ispravi: ASCII u njemu zauzima, kao i ranije, samo jedan bajt; kodovi iz 0x80 do 0x7FF - dva bajta; od 0x800 do 0xFFFF - tri i od 0x10000 do 0x10FFFF - četiri. S jedne strane, latinica je postala dobra: vratila se kompatibilnost sa ASCII-om, a distribucija je ravnomjernije "rasprostranjena" od 1 do 4 bajta. Ali alfabete osim latinice, nažalost, nemaju nikakve koristi u poređenju sa UTF-16, a mnogima su sada potrebna tri bajta umjesto dva - raspon koji pokriva dvobajtni zapis smanjio se za 32 puta, s 0xFFFF do 0x7FF, a u njega nisu uključeni ni kineski ni, na primjer, gruzijski. Ćirilica i pet drugih abeceda - ura - sreća, 2 bajta po znaku.

Zašto se to dešava? Pogledajmo kako UTF-8 predstavlja kodove znakova:
Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Direktno za predstavljanje brojeva ovdje se koriste bitovi označeni simbolom x. Može se vidjeti da u dvobajtnom zapisu postoji samo 11 takvih bitova (od 16). Vodeći bitovi ovdje imaju samo pomoćnu funkciju. U slučaju zapisa od četiri bajta, 21 od 32 bita se dodeljuje za broj kodne tačke - čini se da bi tri bajta (koji daju ukupno 24 bita) bila dovoljna, ali servisni markeri jedu previše.

Je li ovo loše? Ne baš. S jedne strane, ako nam je mnogo stalo do prostora, imamo algoritme kompresije koji lako mogu eliminirati svu dodatnu entropiju i redundantnost. S druge strane, cilj Unicode-a je bio da obezbijedi najuniverzalnije moguće kodiranje. Na primjer, možemo povjeriti liniju kodiranu u UTF-8 kodu koji je ranije radio samo sa ASCII, i ne plašiti se da će vidjeti znak iz ASCII raspona kojeg zapravo nema (na kraju krajeva, u UTF-8 svi bajtova koji počinju od nultog bita - to je upravo ono što je ASCII). A ako odjednom poželimo da odsečemo mali rep od velikog niza, a da ga ne dešifrujemo od samog početka (ili da vratimo deo informacija nakon oštećenog dela), lako nam je da pronađemo pomak gde počinje znak (dovoljno je da preskočite bajtove koji imaju prefiks bita 10).

Zašto onda izmišljati nešto novo?

Istovremeno, povremeno postoje situacije kada su algoritmi kompresije kao što je deflate slabo primjenjivi, ali želite postići kompaktno skladištenje nizova. Lično sam naišao na ovaj problem kada sam razmišljao o izgradnji komprimirano stablo prefiksa za veliki rečnik koji uključuje reči na proizvoljnim jezicima. S jedne strane, svaka riječ je vrlo kratka, pa će njeno kompresovanje biti neučinkovito. S druge strane, implementacija stabla koju sam razmatrao je dizajnirana tako da svaki bajt pohranjenog niza generiše poseban vrh stabla, tako da je minimiziranje njihovog broja bilo vrlo korisno. U mojoj biblioteci Az.js (Kao u pimorfija2, na kojem se zasniva) sličan problem se može jednostavno riješiti - stringovi upakirani u DAWG-rečnik, pohranjen tamo u dobri stari CP1251. Ali, kao što je lako razumjeti, ovo dobro funkcionira samo za ograničenu abecedu - red na kineskom se ne može dodati u takav rječnik.

Zasebno, želio bih napomenuti još jednu neugodnu nijansu koja se javlja kada se koristi UTF-8 u takvoj strukturi podataka. Slika iznad pokazuje da kada je znak napisan kao dva bajta, bitovi koji se odnose na njegov broj ne dolaze u niz, već su razdvojeni parom bitova 10 u sredini: 110xxxxx 10xxxxxx. Zbog toga, kada se nižih 6 bitova drugog bajta prelije u kodu znakova (tj. dođe do prijelaza 1011111110000000), tada se mijenja i prvi bajt. Ispostavilo se da je slovo "p" označeno bajtovima 0xD0 0xBF, a sljedeće "r" je već 0xD1 0x80. U stablu prefiksa, ovo dovodi do cijepanja roditeljskog čvora na dva - jedan za prefiks 0xD0, i još jedan za 0xD1 (iako je čitava ćirilica mogla biti kodirana samo drugim bajtom).

Šta sam dobio

Suočen sa ovim problemom, odlučio sam da vežbam igranje igrica sa bitovima, a da se u isto vreme malo bolje upoznam sa strukturom Unicode-a u celini. Rezultat je bio UTF-C format kodiranja ("C" za kompaktni), koji ne troši više od 3 bajta po kodnoj točki, a vrlo često vam omogućava da trošite samo jedan dodatni bajt za cijeli kodirani red. To dovodi do činjenice da se na mnogim ne-ASCII alfabetima ispostavlja da postoji takvo kodiranje 30-60% kompaktniji od UTF-8.

Predstavio sam primjere implementacije algoritama kodiranja i dekodiranja u formi JavaScript i Go biblioteke, možete ih slobodno koristiti u svom kodu. Ali ipak ću naglasiti da u određenom smislu ovaj format ostaje "bicikl" i ne preporučujem da ga koristite a da ne shvatate zašto vam je to potrebno. Ovo je još uvijek više eksperiment nego ozbiljno "poboljšanje UTF-8". Ipak, kod je tamo napisan uredno, sažeto, sa velikim brojem komentara i pokrivenosti testom.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Rezultati testa i poređenje sa UTF-8

I ja jesam demo stranica, gdje možete ocijeniti performanse algoritma, a zatim ću vam reći više o njegovim principima i procesu razvoja.

Uklanjanje suvišnih bitova

Uzeo sam UTF-8 kao osnovu, naravno. Prva i najočiglednija stvar koja se može promijeniti u njemu je smanjenje broja servisnih bitova u svakom bajtu. Na primjer, prvi bajt u UTF-8 uvijek počinje sa bilo kojim 0, ili sa 11 - prefiks 10 Imaju ga samo sljedeći bajtovi. Zamenimo prefiks 11 na 1, a za sljedeće bajtove u potpunosti ćemo ukloniti prefikse. Šta će se desiti?

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

Čekaj, gdje je zapis od četiri bajta? Ali to više nije potrebno - pri pisanju u tri bajta sada imamo na raspolaganju 21 bit i to je dovoljno za sve brojeve do 0x10FFFF.

Šta smo mi ovde žrtvovali? Najvažnija stvar je otkrivanje granica karaktera sa proizvoljne lokacije u baferu. Ne možemo pokazati na proizvoljan bajt i pronaći početak sljedećeg znaka iz njega. Ovo je ograničenje našeg formata, ali u praksi je to rijetko potrebno. Obično smo u mogućnosti proći kroz bafer od samog početka (posebno kada su u pitanju kratke linije).

Situacija s pokrivanjem jezika sa 2 bajta također je postala bolja: sada dvobajtni format daje raspon od 14 bita, a to su kodovi do 0x3FFF. Kinezi nemaju sreće (njihovi karakteri se uglavnom kreću od 0x4E00 do 0x9FFF), ali Gruzijci i mnogi drugi narodi se više zabavljaju - njihovi jezici se također uklapaju u 2 bajta po znaku.

Unesite stanje kodera

Razmislimo sada o svojstvima samih linija. Rječnik najčešće sadrži riječi napisane znakovima iste abecede, a to vrijedi i za mnoge druge tekstove. Bilo bi dobro da jednom naznačite ovo pismo, a zatim naznačite samo broj slova u njemu. Da vidimo da li će nam raspored znakova u Unicode tabeli pomoći.

Kao što je gore spomenuto, Unicode je podijeljen na avion 65536 kodova svaki. Ali ovo nije baš korisna podjela (kao što je već rečeno, najčešće smo u nultoj ravni). Zanimljivija je podjela po blokova. Ovi opsezi više nemaju fiksnu dužinu, već su značajniji - u pravilu svaki kombinuje znakove iz iste abecede.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Blok koji sadrži znakove bengalskog alfabeta. Nažalost, iz povijesnih razloga, ovo je primjer ne baš gustog pakovanja - 96 znakova je haotično razbacano po 128 blok kodnih točaka.

Počeci blokova i njihove veličine su uvijek višestruki od 16 - to se radi jednostavno radi praktičnosti. Osim toga, mnogi blokovi počinju i završavaju na vrijednostima koje su višestruke od 128 ili čak 256 - na primjer, osnovno ćirilično pismo zauzima 256 bajtova od 0x0400 do 0x04FF. Ovo je prilično zgodno: ako jednom sačuvamo prefiks 0x04, tada se bilo koji ćirilični znak može napisati u jednom bajtu. Istina, na ovaj način ćemo izgubiti priliku da se vratimo na ASCII (i na bilo koje druge znakove općenito). Stoga radimo ovo:

  1. Dva bajta 10yyyyyy yxxxxxxx ne samo da označavaju simbol brojem yyyyyy yxxxxxxx, ali i promjene aktuelna abeceda na yyyyyy y0000000 (tj. pamtimo sve bitove osim onih najmanje značajnih 7 bita);
  2. Jedan bajt 0xxxxxxx ovo je znak trenutnog alfabeta. Samo ga treba dodati ofsetu kojeg smo zapamtili u koraku 1. Iako nismo promijenili abecedu, pomak je nula, tako da smo zadržali kompatibilnost sa ASCII.

Isto tako za kodove koji zahtijevaju 3 bajta:

  1. Tri bajta 110yyyyy yxxxxxxx xxxxxxxx označava simbol sa brojem yyyyyy yxxxxxxx xxxxxxxx, promijeniti aktuelna abeceda na yyyyyy y0000000 00000000 (sjećao se svega osim mlađih 15 bita), i označite polje u kojem se sada nalazimo dugo mod (kada se abeceda vrati u dvobajtnu, resetovaćemo ovu zastavicu);
  2. Dva bajta 0xxxxxxx xxxxxxxx u dugom modu to je znak tekuće abecede. Slično, dodajemo ga sa pomakom iz koraka 1. Jedina razlika je u tome što sada čitamo dva bajta (jer smo se prebacili na ovaj način).

Zvuči dobro: sada dok trebamo kodirati znakove iz istog 7-bitnog Unicode raspona, trošimo 1 dodatni bajt na početku i ukupno jedan bajt po karakteru.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Radi na jednoj od ranijih verzija. Već često nadmašuje UTF-8, ali još uvijek ima prostora za poboljšanje.

sta je gore? Prvo, imamo uslov, tj trenutni pomak abecede i potvrdni okvir dugi mod. Ovo nas dodatno ograničava: sada isti znakovi mogu biti različito kodirani u različitim kontekstima. Pretraživanje podstringova, na primjer, morat će se obaviti uzimajući to u obzir, a ne samo poređenjem bajtova. Drugo, čim smo promijenili abecedu, postalo je loše sa kodiranjem ASCII znakova (a ovo nije samo latinica, već i osnovna interpunkcija, uključujući i razmake) - zahtijevaju ponovno promjenu abecede na 0, tj. opet dodatni bajt (i onda još jedan da se vratimo na našu glavnu poentu).

Jedna azbuka je dobra, dva je bolja

Pokušajmo malo promijeniti naše bitne prefikse, stišćući još jedan na tri gore opisana:

0xxxxxxx — 1 bajt u normalnom režimu, 2 u dugom režimu
11xxxxxx — 1 bajt
100xxxxx xxxxxxxx - 2 bajta
101xxxxx xxxxxxxx xxxxxxxx - 3 bajta

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8

Sada u dvobajtnom zapisu postoji jedan manje dostupan bit - kod pokazuje do 0x1FFF, a ne 0x3FFF. Međutim, i dalje je primjetno veći nego u dvobajtnim UTF-8 kodovima, najčešći jezici se i dalje uklapaju, najuočljiviji gubitak je ispao hiragana и katakana, Japanci su tužni.

Šta je ovaj novi kod? 11xxxxxx? Ovo je mala „sprema“ veličine 64 znaka, nadopunjuje našu glavnu abecedu, pa sam je nazvao pomoćnom (pomoćni) abeceda. Kada promijenimo trenutni alfabet, dio stare abecede postaje pomoćni. Na primjer, prešli smo sa ASCII-a na ćirilicu - zaliha sada sadrži 64 karaktera Latinica, brojevi, razmak i zarez (najčešća umetanja u tekstove koji nisu ASCII). Vratite se na ASCII - i glavni dio ćirilice će postati pomoćno pismo.

Zahvaljujući pristupu dvije abecede možemo obraditi veliki broj tekstova uz minimalne troškove za promjenu alfabeta (interpunkcija će najčešće dovesti do povratka na ASCII, ali nakon toga ćemo iz dodatne abecede dobiti mnogo ne-ASCII znakova, bez ponovo prebacivanje).

Bonus: prefiksom podabecede 11xxxxxx i odabirom njegovog početnog pomaka da bude 0xC0, dobijamo delimičnu kompatibilnost sa CP1252. Drugim riječima, mnogi (ali ne svi) zapadnoevropski tekstovi kodirani u CP1252 izgledat će isto u UTF-C.

Ovdje, međutim, nastaje poteškoća: kako dobiti pomoćnu iz glavne abecede? Možete ostaviti isti pomak, ali - nažalost - ovdje Unicode struktura već igra protiv nas. Vrlo često glavni dio abecede nije na početku bloka (na primjer, rusko veliko "A" ima šifru 0x0410, iako ćirilični blok počinje sa 0x0400). Stoga, uzimajući prva 64 znaka u skladište, možemo izgubiti pristup repnom dijelu abecede.

Da bih riješio ovaj problem, ručno sam prošao kroz neke blokove koji odgovaraju različitim jezicima i odredio pomak pomoćnog alfabeta unutar glavnog za njih. Latinica je, kao izuzetak, generalno preuređena kao base64.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8

Završni dodiri

Hajde da konačno razmislimo gde još možemo nešto da poboljšamo.

Imajte na umu da format 101xxxxx xxxxxxxx xxxxxxxx omogućava vam da kodirate brojeve do 0x1FFFFF, a Unicode završava ranije, na 0x10FFFF. Drugim riječima, posljednja kodna tačka će biti predstavljena kao 10110000 11111111 11111111. Stoga možemo reći da ako je prvi bajt u obliku 1011xxxx (Gdje xxxx veće od 0), onda to znači nešto drugo. Na primjer, tamo možete dodati još 15 znakova koji su stalno dostupni za kodiranje u jednom bajtu, ali ja sam odlučio da to učinim drugačije.

Pogledajmo one Unicode blokove koji sada zahtijevaju tri bajta. U osnovi, kao što je već spomenuto, ovo su kineski znakovi - ali teško je išta učiniti s njima, ima ih 21 hiljada. Ali tamo su doletjele i hiragana i katakana - a nema ih više toliko, manje od dvije stotine. A, otkako smo se sjetili Japanaca, tu su i emoji (u stvari, razbacani su na mnogo mjesta u Unicodeu, ali glavni blokovi su u rasponu 0x1F300 - 0x1FBFF). Ako razmislite o činjenici da sada postoje emojiji koji su sastavljeni iz nekoliko kodnih točaka odjednom (na primjer, emoji ‍‍‍Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8 sastoji se od čak 7 kodova!), onda postaje potpuna šteta potrošiti tri bajta na svaki (7×3 = 21 bajt zarad jedne ikone, noćna mora).

Stoga odabiremo nekoliko odabranih raspona koji odgovaraju emoji, hiragani i katakani, prenumerujemo ih u jednu kontinuiranu listu i kodiramo ih kao dva bajta umjesto tri:

1011xxxx xxxxxxxx

Odlično: gore spomenuti emojiJoš jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8, koji se sastoji od 7 kodnih tačaka, zauzima 8 bajtova u UTF-25 i mi ga uklapamo 14 (tačno dva bajta za svaku kodnu tačku). Inače, Habr je odbio da ga svari (i u starom i u novom editoru), pa sam morao da ga ubacim sa slikom.

Pokušajmo riješiti još jedan problem. Kao što se sjećamo, osnovna abeceda je u suštini visokih 6 bita, što imamo na umu i lijepimo za kod svakog sljedećeg dekodiranog simbola. U slučaju kineskih znakova koji su u bloku 0x4E00 - 0x9FFF, ovo je ili bit 0 ili 1. Ovo nije baš zgodno: morat ćemo stalno mijenjati abecedu između ove dvije vrijednosti (tj. potrošiti tri bajta). Ali imajte na umu da u dugom modu, od samog koda možemo oduzeti broj znakova koje kodiramo koristeći kratki način (nakon svih trikova opisanih iznad, ovo je 10240) - tada će se raspon hijeroglifa pomaknuti na 0x2600 - 0x77FF, i u ovom slučaju, u cijelom ovom rasponu, najznačajnijih 6 bitova (od 21) će biti jednako 0. Dakle, sekvence hijeroglifa će koristiti dva bajta po hijeroglifu (što je optimalno za tako veliki raspon), bez uzrokujući prebacivanje abecede.

Alternativna rješenja: SCSU, BOCU-1

Stručnjaci za Unicode, koji su upravo pročitali naslov članka, najvjerovatnije će vas požuriti podsjetiti da direktno među Unicode standardima postoji Standardna šema kompresije za Unicode (SCSU), koji opisuje metodu kodiranja vrlo sličnu onoj opisanoj u članku.

Iskreno priznajem: za njegovo postojanje sam saznao tek nakon što sam bio duboko uronjen u pisanje svoje odluke. Da sam znao za to od početka, vjerovatno bih pokušao da napišem implementaciju umjesto da osmislim vlastiti pristup.

Ono što je zanimljivo jeste da SCSU koristi ideje vrlo slične onima koje sam ja smislio (umjesto koncepta „abecede“ koriste „prozore“, a ima ih više nego što imam). Istovremeno, ovaj format ima i nedostatke: malo je bliži algoritmima kompresije nego kodiranim. Konkretno, standard daje mnoge metode predstavljanja, ali ne kaže kako odabrati optimalnu - za to koder mora koristiti neku vrstu heuristike. Stoga će SCSU koder koji proizvodi dobro pakovanje biti složeniji i glomazniji od mog algoritma.

Poređenja radi, prenio sam relativno jednostavnu implementaciju SCSU-a na JavaScript - u smislu volumena koda ispostavilo se da je uporediv s mojim UTF-C, ali u nekim slučajevima rezultat je bio desetine posto lošiji (ponekad ga može premašiti, ali ne mnogo). Na primjer, tekstovi na hebrejskom i grčkom bili su kodirani UTF-C 60% bolje od SCSU (vjerovatno zbog njihove kompaktne abecede).

Odvojeno, dodat ću da osim SCSU-a postoji i drugi način za kompaktno predstavljanje Unicodea - BOCU-1, ali ima za cilj MIME kompatibilnost (koja mi nije bila potrebna) i ima malo drugačiji pristup kodiranju. Nisam procijenio njegovu efikasnost, ali mi se čini da je malo vjerovatno da će biti veći od SCSU.

Moguća poboljšanja

Algoritam koji sam predstavio nije univerzalan po dizajnu (tu se vjerovatno moji ciljevi najviše razlikuju od ciljeva Unicode konzorcijuma). Već sam spomenuo da je razvijen prvenstveno za jedan zadatak (skladištenje višejezičnog rječnika u stablu prefiksa), a neke od njegovih karakteristika možda nisu pogodne za druge zadatke. Ali činjenica da to nije standard može biti plus - možete ga lako modificirati tako da odgovara vašim potrebama.

Na primjer, na očigledan način možete se riješiti prisutnosti stanja, napraviti kodiranje bez stanja - samo nemojte ažurirati varijable offs, auxOffs и is21Bit u koderu i dekoderu. U ovom slučaju, neće biti moguće efikasno pakirati nizove znakova iste abecede, ali će postojati garancija da je isti znak uvijek kodiran istim bajtovima, bez obzira na kontekst.

Osim toga, možete prilagoditi koder za određeni jezik promjenom zadanog stanja - na primjer, fokusirajući se na ruske tekstove, postavite koder i dekoder na početku offs = 0x0400 и auxOffs = 0. Ovo posebno ima smisla u slučaju načina rada bez državljanstva. Općenito, ovo će biti slično korištenju starog osmobitnog kodiranja, ali bez uklanjanja mogućnosti umetanja znakova iz cijelog Unicodea po potrebi.

Još jedan nedostatak koji je ranije spomenut je da u velikom tekstu kodiranom u UTF-C ne postoji brz način da se pronađe granica karaktera najbliža proizvoljnom bajtu. Ako odsiječete posljednjih, recimo, 100 bajtova iz kodiranog bafera, rizikujete da dobijete smeće s kojim ne možete ništa učiniti. Kodiranje nije dizajnirano za pohranjivanje višegigabajtnih dnevnika, ali općenito se to može ispraviti. Byte 0xBF nikada se ne smije pojaviti kao prvi bajt (ali može biti drugi ili treći). Stoga, kada kodirate, možete umetnuti sekvencu 0xBF 0xBF 0xBF svakih, recimo, 10 KB - tada će, ako trebate pronaći granicu, biti dovoljno skenirati odabrani komad dok se ne pronađe sličan marker. Nakon posljednjeg 0xBF garantovano je početak lika. (Prilikom dekodiranja, ovaj niz od tri bajta će se, naravno, morati zanemariti.)

Sumirati

Ako ste čitali do sada, čestitam! Nadam se da ste, poput mene, naučili nešto novo (ili osvježili pamćenje) o strukturi Unicode-a.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Demo stranica. Primjer hebrejskog pokazuje prednosti u odnosu na UTF-8 i SCSU.

Gore opisano istraživanje ne treba smatrati zadiranjem u standarde. Međutim, generalno sam zadovoljan rezultatima svog rada, tako da sam zadovoljan njima podeliti: na primjer, minimizirana JS biblioteka je teška samo 1710 bajtova (i nema ovisnosti, naravno). Kao što sam već pomenuo, njen rad se može naći na demo stranica (postoji i skup tekstova na kojima se može porediti sa UTF-8 i SCSU).

Na kraju, još jednom ću skrenuti pažnju na slučajeve u kojima se koristi UTF-C ne vredi:

  • Ako su vaši redovi dovoljno dugi (od 100-200 karaktera). U ovom slučaju, trebali biste razmisliti o korištenju algoritama kompresije kao što je deflate.
  • Ako trebaš ASCII transparentnost, odnosno važno vam je da kodirane sekvence ne sadrže ASCII kodove koji nisu bili u izvornom nizu. Potreba za ovim se može izbjeći ako, prilikom interakcije sa API-jima trećih strana (na primjer, rad s bazom podataka), prosledite rezultat kodiranja kao apstraktni skup bajtova, a ne kao nizove. U suprotnom, rizikujete da dobijete neočekivane ranjivosti.
  • Ako želite biti u mogućnosti da brzo pronađete granice znakova na proizvoljnom pomaku (na primjer, kada je dio reda oštećen). To se može učiniti, ali samo skeniranjem linije od početka (ili primjenom modifikacije opisane u prethodnom odjeljku).
  • Ako trebate brzo izvršiti operacije nad sadržajem nizova (sortirati ih, potražiti podnizove u njima, spojiti). Ovo zahtijeva da se nizovi prvo dekodiraju, tako da će UTF-C u ovim slučajevima biti sporiji od UTF-8 (ali brži od algoritama kompresije). Pošto je isti niz uvijek kodiran na isti način, nije potrebno tačno poređenje dekodiranja i može se obaviti na bazi bajta po bajt.

update: korisnik Tyomitch u komentarima ispod objavio grafikon koji naglašava granice primenljivosti UTF-C. Pokazuje da je UTF-C efikasniji od algoritma kompresije opšte namene (varijacija LZW) sve dok je pakovani niz kraći ~140 karaktera (međutim, napominjem da je poređenje izvršeno na jednom tekstu; za druge jezike rezultat se može razlikovati).
Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8

izvor: www.habr.com

Dodajte komentar