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, Unicode će gotovo uvijek biti pravo rješenje. Specifična metoda reprezentacije ovisi o kontekstu, ali najčešće i ovdje postoji univerzalni odgovor - UTF-8. Dobra stvar kod njega je što vam omogućuje korištenje svih Unicode znakova bez trošenja previše puno bajtova u većini slučajeva. Istina, za jezike koji koriste više od latinice, "ne previše" je barem dva bajta po znaku. Možemo li učiniti bolje bez vraćanja na pretpovijesna 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ćuje pohranjivanje redaka na većini svjetskih jezika bez dodavanja redundancije koja je u UTF-8.

Odricanje. 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 se ne smije koristiti za interakciju s API-jima trećih strana (koji za to niti ne znaju). Najčešće su algoritmi za kompresiju opće namjene (na primjer, deflate) prikladni za kompaktnu pohranu velikih količina tekstualnih podataka. Osim toga, već u procesu kreiranja mog rješenja, pronašao sam postojeći standard u samom Unicodeu, koji rješava isti problem - nešto je kompliciraniji (a često i gori), ali ipak je prihvaćeni standard, a ne samo stavljen zajedno na koljenu. Pričat ću ti i o njemu.

O Unicodeu i UTF-8

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

Kao što znate, 8-bitno kodiranje je bilo popularno. Kod njih je sve bilo jednostavno: 256 znakova može se numerirati brojevima od 0 do 255, a brojevi od 0 do 255 očito se mogu prikazati kao jedan bajt. Ako se vratimo na sam početak, ASCII kodiranje je potpuno ograničeno na 7 bitova, pa je najvažniji bit u njegovoj bajt reprezentaciji nula, a većina 8-bitnih kodiranja je kompatibilna s njim (razlikuju se samo u “gornjem” dio, gdje je najvažniji bit jedan).

Kako se Unicode razlikuje od tih kodiranja i zašto je toliko mnogo specifičnih prikaza povezano s njim - UTF-8, UTF-16 (BE i LE), UTF-32? Posložimo redom.

Osnovni Unicode standard opisuje samo korespondenciju između znakova (i u nekim slučajevima, pojedinačnih komponenti znakova) i njihovih brojeva. I postoji puno mogućih brojeva u ovom standardu - od 0x00 na 0x10FFFF (1 komada). Kad bismo u varijablu htjeli staviti broj u takvom rasponu, ne bi nam bila dovoljna ni 114 ni 112 bajta. A budući da naši procesori nisu baš dizajnirani za rad s trobajtnim brojevima, bili bismo prisiljeni koristiti čak 1 bajta po znaku! Ovo je UTF-2, ali upravo zbog te “rasipnosti” ovaj format nije popularan.

Srećom, redoslijed znakova unutar Unicodea nije slučajan. Cijeli njihov skup podijeljen je na 17"avionima", od kojih svaki sadrži 65536 (0x10000) «kodne točke" Koncept "kodne točke" ovdje je jednostavan broj znaka, koji mu je dodijelio Unicode. Ali, kao što je gore spomenuto, u Unicodeu nisu numerirani samo pojedinačni znakovi, već i njihove komponente i službene oznake (a ponekad uopće ništa ne odgovara broju - možda za sada, ali za nas to nije toliko važno), tako da točnije je uvijek govoriti konkretno o broju samih brojeva, a ne o simbolima. Međutim, u nastavku ću, radi sažetosti, često koristiti riječ "simbol", podrazumijevajući pojam "kodna točka".

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

Ono što je najznačajnije je da sva glavna "pulpa" leži u nultoj ravnini, zove se "Osnovna višejezična ravnina". Ako redak sadrži tekst na jednom od modernih jezika (uključujući kineski), nećete ići dalje od ove ravnine. Ali ne možete odrezati ni ostatak Unicodea - na primjer, emoji se uglavnom nalaze na kraju sljedeći avion,"Dodatna višejezična ravnina"(proteže se od 0x10000 na 0x1FFFF). Dakle, UTF-16 radi ovo: svi znakovi koji spadaju unutar Osnovna višejezična ravnina, su kodirani "kakvi jesu" s odgovarajućim dvobajtnim brojem. Međutim, neki od brojeva u ovom rasponu uopće ne označavaju određene znakove, već pokazuju da nakon ovog para bajtova trebamo razmotriti još jedan - kombiniranjem vrijednosti ova četiri bajta zajedno, dobivamo 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 točki". Ovo je bolje od korištenja četiri bajta cijelo vrijeme, ali latinični (i drugi ASCII znakovi) kada se kodiraju na ovaj način troše pola prostora na nule. UTF-8 je dizajniran da to ispravi: ASCII u njemu zauzima, kao i prije, samo jedan bajt; šifre iz 0x80 na 0x7FF - dva bajta; iz 0x800 na 0xFFFF - tri, i od 0x10000 na 0x10FFFF - četiri. S jedne strane, latinica je postala dobra: kompatibilnost s ASCII-jem se vratila, a distribucija je ravnomjernije "rasprostranjena" od 1 do 4 bajta. Ali alfabeti osim latinice, nažalost, nemaju nikakve prednosti u usporedbi s UTF-16, a mnogi sada zahtijevaju tri bajta umjesto dva - raspon pokriven dvobajtnim zapisom sužen je 32 puta, s 0xFFFF na 0x7FF, a u njega nisu uključeni ni kineski ni npr. gruzijski. Ćirilica i pet drugih pisama - ura - sretno, 2 bajta po znaku.

Zašto se to događa? Pogledajmo kako UTF-8 predstavlja kodove znakova:
Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Bitovi označeni simbolom ovdje se koriste izravno za predstavljanje brojeva x. Vidi se 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 je dodijeljen za broj kodne točke - čini se da bi tri bajta (što ukupno daje 24 bita) bila dovoljna, ali servisni markeri jedu previše.

Je li ovo loše? Ne baš. S jedne strane, ako nam je jako stalo do prostora, imamo algoritme kompresije koji mogu lako eliminirati svu dodatnu entropiju i redundanciju. S druge strane, cilj Unicodea bio je pružiti što univerzalnije kodiranje. Na primjer, redak kodiran u UTF-8 možemo povjeriti kodu koji je prije radio samo s ASCII-jem, a ne bojati se da će vidjeti znak iz ASCII raspona kojeg zapravo nema (uostalom, u UTF-8 svi bajtova počevši od nultog bita - to je upravo ono što je ASCII). A ako iznenada poželimo odrezati mali rep od velikog niza bez dekodiranja od samog početka (ili vratiti dio informacija nakon oštećenog dijela), lako nam je pronaći pomak gdje znak počinje (dovoljno je za preskakanje bajtova koji imaju prefiks bit 10).

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

Istodobno, povremeno postoje situacije kada su algoritmi kompresije poput deflate slabo primjenjivi, ali želite postići kompaktnu pohranu nizova. Osobno sam se susreo s ovim problemom kada sam razmišljao o gradnji komprimirano stablo prefiksa za veliki rječnik koji uključuje riječi u proizvoljnim jezicima. S jedne strane, svaka je riječ vrlo kratka, pa će njeno sažimanje biti neučinkovito. S druge strane, implementacija stabla koju sam smatrao dizajnirana je tako da svaki bajt pohranjenog niza generira zaseban vrh stabla, tako da je smanjivanje njihovog broja bilo vrlo korisno. U mojoj knjižnici Az.js (Kao u pimorfija2, na kojem se temelji) sličan problem može se jednostavno riješiti - stringovi upakirani u Dawg-rječnik, tamo pohranjen u dobri stari CP1251. Ali, kao što je lako razumjeti, ovo dobro funkcionira samo za ograničeni alfabet - takvom rječniku ne može se dodati redak na kineskom.

Zasebno bih želio primijetiti još jednu neugodnu nijansu koja nastaje pri korištenju UTF-8 u takvoj strukturi podataka. Gornja slika pokazuje da kada je znak napisan kao dva bajta, bitovi koji se odnose na njegov broj ne dolaze u nizu, već su odvojeni parom bitova 10 u sredini: 110xxxxx 10xxxxxx. Zbog toga, kada se nižih 6 bitova drugog bajta prelijeva u kodu znaka (tj. dolazi do prijelaza 1011111110000000), tada se mijenja i prvi bajt. Ispada da je slovo "p" označeno bajtovima 0xD0 0xBF, a sljedeće "r" je već 0xD1 0x80. U stablu prefiksa to dovodi do cijepanja nadređenog čvora na dva - jedan za prefiks 0xD0, a drugi za 0xD1 (iako se cijela ćirilica može kodirati samo drugim bajtom).

Što sam dobio

Suočen s ovim problemom, odlučio sam vježbati igranje igrica s bitovima, a ujedno i malo bolje upoznati strukturu Unicodea u cjelini. Rezultat je bio UTF-C format kodiranja ("C" za kompaktni), koji ne troši više od 3 bajta po kodnoj točki i vrlo često vam omogućuje da potrošite samo jedan dodatni bajt za cijelu kodiranu liniju. To dovodi do činjenice da se na mnogim ne-ASCII abecedama takvo kodiranje ispostavlja 30-60% kompaktniji od UTF-8.

U obrascu sam prikazao primjere implementacije algoritama za kodiranje i dekodiranje JavaScript i Go biblioteke, možete ih slobodno koristiti u svom kodu. Ali ipak ću naglasiti da ovaj format u određenom smislu ostaje “bicikl” i ne preporučam njegovu upotrebu a da ne shvatite zašto vam je to potrebno. Ovo je još uvijek više eksperiment nego ozbiljno "poboljšanje UTF-8". Unatoč tome, kod je tamo napisan uredno, sažeto, s velikim brojem komentara i pokrivenosti testa.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Rezultati testa i usporedba s UTF-8

I ja jesam demo stranica, gdje možete procijeniti 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čitija stvar koja se u njemu može promijeniti je smanjenje broja servisnih bitova u svakom bajtu. Na primjer, prvi bajt u UTF-8 uvijek počinje s bilo 0, ili sa 11 - prefiks 10 Imaju ga samo sljedeći bajtovi. Zamijenimo prefiks 11 na 1, a za sljedeće bajtove potpuno ćemo ukloniti prefikse. Što će se dogoditi?

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

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

Što smo mi ovdje žrtvovali? Najvažnija stvar je otkrivanje granica znakova s ​​proizvoljnog mjesta u međuspremniku. Ne možemo pokazati na proizvoljni bajt i iz njega pronaći početak sljedećeg znaka. Ovo je ograničenje našeg formata, ali u praksi je to rijetko potrebno. Obično možemo proći kroz međuspremnik od samog početka (posebno kada su u pitanju kratke linije).

Situacija s pokrivanjem jezika s 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 uglavnom variraju od 0x4E00 na 0x9FFF), ali Gruzijci i mnogi drugi narodi imaju više zabave - njihovi jezici također stanu u 2 bajta po znaku.

Unesite stanje kodera

Razmislimo sada o svojstvima samih linija. Rječnik najčešće sadrži riječi napisane slovima iste abecede, a to vrijedi i za mnoge druge tekstove. Bilo bi dobro jednom označiti ovu abecedu, a zatim navesti samo broj slova unutar nje. Da vidimo hoće li nam raspored znakova u Unicode tablici 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 ravnini). Zanimljivija je podjela po blokova. Ovi rasponi više nemaju fiksnu duljinu i imaju više smisla - u pravilu svaki kombinira znakove iz iste abecede.

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Blok koji sadrži znakove bengalske abecede. Nažalost, zbog povijesnih razloga, ovo je primjer ne baš gustog pakiranja - 96 znakova kaotično je razbacano u 128 blok kodnih točaka.

Počeci blokova i njihove veličine uvijek su višekratnici 16 - to je učinjeno jednostavno radi praktičnosti. Osim toga, mnogi blokovi počinju i završavaju vrijednostima koje su višekratnici 128 ili čak 256 - na primjer, osnovna ćirilica zauzima 256 bajtova od 0x0400 na 0x04FF. Ovo je sasvim zgodno: ako jednom spremimo 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 sve druge znakove općenito). Stoga radimo ovo:

  1. Dva bajta 10yyyyyy yxxxxxxx ne samo da označavaju simbol brojem yyyyyy yxxxxxxx, ali i promijeniti trenutna abeceda na yyyyyy y0000000 (tj. pamtimo sve bitove osim onih najmanjeg značaja 7 bit);
  2. Jedan bajt 0xxxxxxx ovo je znak trenutne abecede. Samo ga treba dodati pomaku koji smo zapamtili u koraku 1. Iako nismo promijenili abecedu, pomak je nula, tako da smo zadržali kompatibilnost s ASCII-jem.

Isto tako za kodove koji zahtijevaju 3 bajta:

  1. Tri bajta 110yyyyy yxxxxxxx xxxxxxxx označite simbol brojem yyyyyy yxxxxxxx xxxxxxxx, promijeniti trenutna abeceda na yyyyyy y0000000 00000000 (sjetio se svega osim mlađih 15 bit), i potvrdite okvir u kojem se sada nalazimo dugo način rada (kada mijenjamo abecedu natrag u dvobajtnu, poništit ćemo ovu zastavicu);
  2. Dva bajta 0xxxxxxx xxxxxxxx u dugom načinu rada to je znak trenutne abecede. Slično, dodajemo ga s pomakom iz koraka 1. Jedina je razlika što sada čitamo dva bajta (jer smo se prebacili na ovaj način).

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

Još jedan bicikl: pohranjujemo Unicode nizove 30-60% kompaktnije od UTF-8
Rad iz jedne od ranijih verzija. Već često pobjeđuje UTF-8, ali još ima prostora za napredak.

Što je gore? Prvo, imamo uvjet, naime trenutni abecedni pomak i potvrdni okvir dug način rada. To nas dodatno ograničava: sada se isti znakovi mogu različito kodirati u različitim kontekstima. Traženje podnizova, na primjer, morat će se obaviti uzimajući to u obzir, a ne samo usporedbom bajtova. Drugo, čim smo promijenili abecedu, postalo je loše s kodiranjem ASCII znakova (a to nije samo latinica, već i osnovna interpunkcija, uključujući razmake) - zahtijevaju promjenu abecede ponovno na 0, tj. ponovno dodatni bajt (a zatim još jedan da se vratimo na našu glavnu točku).

Jedna abeceda je dobra, dvije su bolje

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

0xxxxxxx — 1 bajt u normalnom načinu rada, 2 u dugom načinu rada
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 dostupni bit manje - kodne točke do 0x1FFFI ne 0x3FFF. Međutim, još uvijek je primjetno veći nego u dvobajtnim UTF-8 kodovima, većina uobičajenih jezika i dalje se uklapa, najuočljiviji gubitak je ispao hiragana и katakana, Japanci su tužni.

Kakav je to novi kod? 11xxxxxx? Ovo je mala "zaliha" od 64 znaka, nadopunjuje našu glavnu abecedu, pa sam je nazvao pomoćnom (pomoćni) abeceda. Kada promijenimo trenutni alfabet, dio starog alfabeta postaje pomoćni. Na primjer, prebacili smo se s ASCII na ćirilicu - spremište sada sadrži 64 znaka koji sadrže Latinica, brojevi, razmak i zarez (najčešće umetanje u ne-ASCII tekstove). Vratite se na ASCII - i glavni dio ćirilice postat će pomoćna abeceda.

Zahvaljujući pristupu dvama abecedama, možemo obraditi veliki broj tekstova uz minimalne troškove mijenjanja abecede (interpunkcija će najčešće dovesti do povratka na ASCII, ali nakon toga ćemo dobiti mnoge ne-ASCII znakove iz dodatne abecede, bez ponovno prebacivanje).

Bonus: postavljanje predznaka podabecede 11xxxxxx i odabir njegovog početnog pomaka koji će biti 0xC0, dobivamo djelomičnu kompatibilnost s CP1252. Drugim riječima, mnogi (ali ne svi) zapadnoeuropski 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 kod 0x0410, iako ćirilični blok počinje s 0x0400). Stoga, uzimajući prva 64 znaka u zalihu, možemo izgubiti pristup repnom dijelu abecede.

Kako bih riješio ovaj problem, ručno sam prošao kroz neke blokove koji odgovaraju različitim jezicima i odredio pomak pomoćne abecede unutar glavne za njih. Latinica je, kao iznimka, općenito preuređena kao base64.

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

Završni detalji

Ajmo konačno razmisliti gdje još možemo nešto poboljšati.

Imajte na umu da format 101xxxxx xxxxxxxx xxxxxxxx omogućuje kodiranje brojeva do 0x1FFFFF, a Unicode završava ranije, na 0x10FFFF. Drugim riječima, posljednja točka koda bit će predstavljena kao 10110000 11111111 11111111. Stoga možemo reći da ako je prvi bajt oblika 1011xxxx (gdje xxxx veći 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 odlučio sam to učiniti drugačije.

Pogledajmo sada te Unicode blokove koji zahtijevaju tri bajta. U osnovi, kao što je već spomenuto, ovo su kineski znakovi - ali teško je bilo što učiniti s njima, ima ih 21 tisuća. Ali tamo su letjele i hiragana i katakana - a njih više nema toliko, manje od dvije stotine. A, budući da smo se sjetili Japanaca, tu su i emojiji (u stvari, oni su razbacani na mnogim mjestima u Unicodeu, ali glavni blokovi su u rasponu 0x1F300 - 0x1FBFF). Ako razmislite o činjenici da sada postoje emojiji koji su sastavljeni od nekoliko točaka koda odjednom (na primjer, emotikoni ‍‍‍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 za jednu ikonu, noćna mora).

Stoga odabiremo nekoliko odabranih raspona koji odgovaraju emojijima, hiragani i katakani, ponovno ih numeriramo u jedan kontinuirani popis i kodiramo ih kao dva bajta umjesto tri:

1011xxxx xxxxxxxx

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

Pokušajmo riješiti još jedan problem. Kao što se sjećamo, osnovna abeceda je u biti visokih 6 bita, koje imamo na umu i lijepimo na kod svakog sljedećeg dekodiranog simbola. U slučaju kineskih znakova koji su u bloku 0x4E00 - 0x9FFF, ovo je 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 načinu rada od samog koda možemo oduzeti broj znakova koje kodiramo pomoću kratkog načina (nakon svih gore opisanih trikova, to 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) bit će jednako 0. Dakle, nizovi hijeroglifa će koristiti dva bajta po hijeroglifu (što je optimalno za tako veliki raspon), bez uzrokujući promjene abecede.

Alternativna rješenja: SCSU, BOCU-1

Stručnjaci za Unicode, nakon što su upravo pročitali naslov članka, najvjerojatnije će vas požuriti podsjetiti da se izravno među Unicode standardima nalazi Standardna shema kompresije za Unicode (SCSU), koji opisuje metodu kodiranja vrlo sličnu onoj opisanoj u članku.

Iskreno priznajem: za njegovo postojanje saznao sam tek nakon što sam se duboko udubio u pisanje svoje odluke. Da sam znao za to od početka, vjerojatno bih pokušao napisati implementaciju umjesto da osmislim vlastiti pristup.

Zanimljivo je da SCSU koristi ideje vrlo slične onima do kojih sam ja sam došao (umjesto koncepta “abecede” koriste “prozore”, a njih je više nego što ih ja imam). U isto vrijeme, ovaj format ima i nedostatke: malo je bliži algoritmima kompresije nego algoritmima kodiranja. Konkretno, standard daje mnoge metode predstavljanja, ali ne govori kako odabrati optimalnu - za to koder mora koristiti neku vrstu heuristike. Stoga će SCSU koder koji proizvodi dobro pakiranje biti složeniji i glomazniji od mog algoritma.

Za usporedbu, prenio sam relativno jednostavnu implementaciju SCSU u JavaScript - u smislu volumena koda pokazalo se da je usporediva s mojim UTF-C, ali u nekim slučajevima rezultat je bio desetke posto lošiji (ponekad ga može premašiti, ali ne puno). Na primjer, tekstovi na hebrejskom i grčkom bili su kodirani pomoću UTF-C 60% bolji od SCSU (vjerojatno zbog njihove kompaktne abecede).

Zasebno ću dodati da osim SCSU postoji još jedan način kompaktnog predstavljanja Unicodea - BOCU-1, ali cilja na MIME kompatibilnost (koja mi nije trebala) i ima malo drugačiji pristup kodiranju. Nisam procijenio njegovu učinkovitost, ali čini mi se da je malo vjerojatno da će biti veća od SCSU.

Moguća poboljšanja

Algoritam koji sam predstavio nije univerzalan po dizajnu (ovdje se moji ciljevi vjerojatno najviše razlikuju od ciljeva Unicode konzorcija). Već sam spomenuo da je prvenstveno razvijen za jedan zadatak (pohranjivanje višejezičnog rječnika u stablu prefiksa), a neke od njegovih značajki možda neće biti prikladne za druge zadatke. Ali činjenica da to nije standard može biti plus - možete ga jednostavno modificirati kako bi odgovarao vašim potrebama.

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

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

Drugi ranije spomenuti nedostatak je da u velikom tekstu kodiranom u UTF-C ne postoji brzi način za pronalaženje granice znaka najbliže proizvoljnom bajtu. Ako odsiječete posljednjih, recimo, 100 bajtova iz kodiranog međuspremnika, riskirate da dobijete smeće s kojim ne možete učiniti ništa. Kodiranje nije dizajnirano za pohranu zapisa od više gigabajta, ali općenito se to može ispraviti. Bajt 0xBF nikada se ne smije pojaviti kao prvi bajt (ali može biti drugi ili treći). Stoga, prilikom kodiranja, možete umetnuti niz 0xBF 0xBF 0xBF svakih, recimo, 10 KB - tada, ako trebate pronaći granicu, bit će dovoljno skenirati odabrani komad dok se ne pronađe sličan marker. Nakon posljednjeg 0xBF zajamčeno je početak lika. (Pri dekodiranju, ovaj niz od tri bajta će se, naravno, morati zanemariti.)

Sažimanje

Ako ste čitali dovde, čestitamo! Nadam se da ste, poput mene, naučili nešto novo (ili osvježili pamćenje) o strukturi Unicodea.

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. No, generalno sam zadovoljan rezultatima svog rada, tako da sam i njima zadovoljan udio: na primjer, umanjena JS biblioteka teži samo 1710 bajtova (i nema ovisnosti, naravno). Kao što sam već spomenula, njezin rad se može pronaći na demo stranica (postoji i skup tekstova na kojima se može usporediti s UTF-8 i SCSU).

Na kraju, još jednom ću skrenuti pozornost na slučajeve u kojima se koristi UTF-C ne vrijedi:

  • Ako su vaši redovi dovoljno dugi (od 100-200 znakova). 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 može se izbjeći ako, prilikom interakcije s API-jima trećih strana (na primjer, rad s bazom podataka), proslijedite rezultat kodiranja kao apstraktni skup bajtova, a ne kao nizove. Inače riskirate dobivanje neočekivanih ranjivosti.
  • Ako želite biti u mogućnosti brzo pronaći granice znakova na proizvoljnom pomaku (na primjer, kada je dio retka oštećen). To se može učiniti, ali samo skeniranjem linije od početka (ili primjenom izmjene opisane u prethodnom odjeljku).
  • Ako trebate brzo izvršiti operacije nad sadržajem stringova (sortirati ih, tražiti podstringove u njima, spajati). Ovo zahtijeva prvo dekodiranje nizova, tako da će UTF-C u tim slučajevima biti sporiji od UTF-8 (ali brži od algoritama kompresije). Budući da je isti niz uvijek kodiran na isti način, točna usporedba dekodiranja nije potrebna i može se napraviti na bazi bajt po bajt.

Update: korisnik Tyomitch u komentarima ispod objavio je grafikon koji ističe granice primjenjivosti UTF-C. Pokazuje da je UTF-C učinkovitiji od algoritma kompresije opće namjene (varijacija LZW-a) sve dok je pakirani niz kraći ~140 znakova (međutim, napominjem da je usporedba provedena 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