Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8

As jy 'n ontwikkelaar is en jy staan ​​voor die taak om 'n enkodering te kies, dan sal Unicode byna altyd die regte oplossing wees. Die spesifieke voorstellingsmetode hang af van die konteks, maar meestal is daar ook hier 'n universele antwoord - UTF-8. Die goeie ding daarvan is dat dit jou toelaat om alle Unicode-karakters te gebruik sonder om te spandeer ook baie grepe in die meeste gevalle. Dit is waar, vir tale wat meer as net die Latynse alfabet gebruik, is "nie te veel nie" ten minste twee grepe per karakter. Kan ons beter doen sonder om terug te keer na prehistoriese enkoderings wat ons beperk tot net 256 beskikbare karakters?

Hieronder stel ek voor om uself vertroud te maak met my poging om hierdie vraag te beantwoord en 'n relatief eenvoudige algoritme te implementeer waarmee u lyne in die meeste tale van die wêreld kan stoor sonder om die oortolligheid wat in UTF-8 is, by te voeg.

Vrywaring. Ek sal dadelik 'n paar belangrike besprekings maak: die beskryfde oplossing word nie as 'n universele plaasvervanger vir UTF-8 aangebied nie, is dit slegs geskik in 'n nou lys van gevalle (meer daaroor hieronder), en in geen geval moet dit gebruik word om met derdeparty-API's te kommunikeer nie (wat nie eers daarvan weet nie). Dikwels is algemene kompressiealgoritmes (byvoorbeeld deflateer) geskik vir kompakte berging van groot volumes teksdata. Daarbenewens, reeds in die proses om my oplossing te skep, het ek 'n bestaande standaard in Unicode self gevind, wat dieselfde probleem oplos - dit is ietwat meer ingewikkeld (en dikwels erger), maar dit is steeds 'n aanvaarde standaard, en nie net saam op die knie. Ek sal jou ook van hom vertel.

Oor Unicode en UTF-8

Om mee te begin, 'n paar woorde oor wat dit is Unicode и UTF-8.

Soos u weet, was 8-bis-enkoderings vroeër gewild. By hulle was alles eenvoudig: 256 karakters kan genommer word met getalle van 0 tot 255, en getalle van 0 tot 255 kan natuurlik as een greep voorgestel word. As ons teruggaan na die heel begin, is die ASCII-enkodering heeltemal beperk tot 7 bisse, so die belangrikste bis in sy greepvoorstelling is nul, en die meeste 8-bis enkoderings is versoenbaar daarmee (hulle verskil slegs in die "boonste" deel, waar die belangrikste deel een is).

Hoe verskil Unicode van daardie enkoderings en hoekom word so baie spesifieke voorstellings daarmee geassosieer - UTF-8, UTF-16 (BE en LE), UTF-32? Kom ons sorteer dit in volgorde uit.

Die basiese Unicode-standaard beskryf slegs die ooreenstemming tussen karakters (en in sommige gevalle individuele komponente van karakters) en hul nommers. En daar is baie moontlike getalle in hierdie standaard - van 0x00 aan 0x10FFFF (1 114 112 stukke). As ons 'n getal in so 'n reeks in 'n veranderlike wil plaas, sal nie 1 of 2 grepe vir ons genoeg wees nie. En aangesien ons verwerkers nie baie ontwerp is om met drie-grepe-getalle te werk nie, sal ons gedwing word om soveel as 4 grepe per karakter te gebruik! Dit is UTF-32, maar dit is juis as gevolg van hierdie "verkwistheid" dat hierdie formaat nie gewild is nie.

Gelukkig is die volgorde van karakters binne Unicode nie lukraak nie. Hulle hele stel is verdeel in 17 "vliegtuie", wat elk 65536 (0x10000) "kode punte" Die konsep van 'n "kodepunt" hier is eenvoudig karakternommer, deur Unicode daaraan toegewys. Maar, soos hierbo genoem, word nie net individuele karakters in Unicode genommer nie, maar ook hul komponente en diensmerke (en soms stem niks met die nommer ooreen nie - miskien vir eers, maar vir ons is dit nie so belangrik nie), so dit is meer korrek praat altyd spesifiek oor die aantal getalle self, en nie simbole nie. In die volgende sal ek egter kortheidshalwe dikwels die woord "simbool" gebruik, wat die term "kodepunt" impliseer.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
Unicode-vliegtuie. Soos jy kan sien, is die meeste daarvan (vliegtuie 4 tot 13) nog ongebruik.

Wat die merkwaardigste is, is dat al die hoof "pulp" in die nulvlak lê, dit word genoem "Basiese veeltalige vliegtuig". As 'n reël teks in een van die moderne tale bevat (insluitend Chinees), sal jy nie verder as hierdie vlak gaan nie. Maar jy kan ook nie die res van Unicode afsny nie - byvoorbeeld, emoji's is hoofsaaklik aan die einde van die volgende vliegtuig,"Aanvullende veeltalige vliegtuig"(dit strek vanaf 0x10000 aan 0x1FFFF). So UTF-16 doen dit: alle karakters val binne Basiese veeltalige vliegtuig, is geënkodeer "soos dit is" met 'n ooreenstemmende twee-grepe-nommer. Sommige van die getalle in hierdie reeks dui egter glad nie spesifieke karakters aan nie, maar dui aan dat ons na hierdie paar grepe nog een moet oorweeg - deur die waardes van hierdie vier grepe saam te kombineer, kry ons 'n getal wat dek die hele geldige Unicode-reeks. Hierdie idee word "surrogaatpare" genoem - jy het dalk van hulle gehoor.

UTF-16 vereis dus twee of (in baie seldsame gevalle) vier grepe per "kodepunt". Dit is beter as om heeltyd vier grepe te gebruik, maar Latyn (en ander ASCII-karakters) wanneer dit op hierdie manier geënkodeer word, mors die helfte van die spasie op nulle. UTF-8 is ontwerp om dit reg te stel: ASCII daarin beslaan, soos voorheen, slegs een greep; kodes van 0x80 aan 0x7FF - twee grepe; van 0x800 aan 0xFFFF - drie, en van 0x10000 aan 0x10FFFF - vier. Aan die een kant het die Latynse alfabet goed geword: verenigbaarheid met ASCII het teruggekeer, en die verspreiding is meer eweredig "verspreid" van 1 tot 4 grepe. Maar ander alfabette as Latyn, helaas, baat op geen manier in vergelyking met UTF-16 nie, en baie benodig nou drie grepe in plaas van twee - die reeks wat deur 'n twee-grepe-rekord gedek word, het met 32 ​​keer vernou, met 0xFFFF aan 0x7FF, en nie Chinees of, byvoorbeeld, Georgies is daarby ingesluit nie. Cyrilliese en vyf ander alfabette - hoera - gelukkig, 2 grepe per karakter.

Hoekom gebeur dit? Kom ons kyk hoe UTF-8 karakterkodes verteenwoordig:
Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
Direk om getalle voor te stel, word stukkies gemerk met die simbool hier gebruik x. Dit kan gesien word dat daar in 'n tweegreeprekord slegs 11 sulke bisse (uit 16) is. Die voorste bisse het hier slegs 'n hulpfunksie. In die geval van 'n vier-grepe rekord word 21 uit 32 bisse vir die kodepuntnommer toegeken - dit wil voorkom asof drie grepe (wat 'n totaal van 24 bisse gee) genoeg sal wees, maar diensmerkers eet te veel op.

Is dit sleg? Nie regtig nie. Aan die een kant, as ons baie omgee vir ruimte, het ons kompressie-algoritmes wat maklik al die ekstra entropie en oortolligheid kan uitskakel. Aan die ander kant was die doel van Unicode om die mees universele kodering moontlik te verskaf. Ons kan byvoorbeeld 'n reël wat in UTF-8 geënkodeer is, toevertrou aan kode wat voorheen net met ASCII gewerk het, en nie bang wees dat dit 'n karakter uit die ASCII-reeks sal sien wat eintlik nie daar is nie (na alles, in UTF-8 alles grepe wat begin met vanaf die nul-bis - dit is presies wat ASCII is). En as ons skielik 'n klein stert van 'n groot tou wil afsny sonder om dit van die begin af te dekodeer (of 'n gedeelte van die inligting na 'n beskadigde gedeelte wil herstel), is dit maklik vir ons om die afwyking te vind waar 'n karakter begin (dis genoeg om grepe oor te slaan wat 'n bietjie voorvoegsel het 10).

Hoekom dan iets nuuts uitdink?

Terselfdertyd is daar soms situasies wanneer kompressie-algoritmes soos deflate swak toepaslik is, maar jy wil kompakte stoor van snare bereik. Persoonlik het ek hierdie probleem teëgekom toe ek daaraan gedink het om te bou saamgeperste voorvoegselboom vir 'n groot woordeboek wat woorde in arbitrêre tale insluit. Aan die een kant is elke woord baie kort, dus sal dit ondoeltreffend wees om dit saam te druk. Aan die ander kant is die boomimplementering wat ek oorweeg het so ontwerp dat elke greep van die gestoorde string 'n aparte boomhoek gegenereer het, so dit was baie nuttig om hul getal te minimaliseer. In my biblioteek Az.js (Soos in pymorfie2, waarop dit gebaseer is) kan 'n soortgelyke probleem eenvoudig opgelos word - stringe ingepak in DAWG-woordeboek, daar gestoor in goeie ou CP1251. Maar, soos dit maklik is om te verstaan, werk dit goed net vir 'n beperkte alfabet - 'n reël in Chinees kan nie by so 'n woordeboek gevoeg word nie.

Afsonderlik wil ek nog 'n onaangename nuanse opmerk wat ontstaan ​​wanneer UTF-8 in so 'n datastruktuur gebruik word. Die prentjie hierbo wys dat wanneer 'n karakter as twee grepe geskryf word, die bisse wat met sy getal verband hou nie in 'n ry kom nie, maar deur 'n paar bisse geskei word 10 in die middel: 110xxxxx 10xxxxxx. As gevolg hiervan, wanneer die onderste 6 bisse van die tweede greep in die karakterkode oorloop (d.w.s. 'n oorgang vind plaas 1011111110000000), dan verander die eerste greep ook. Dit blyk dat die letter "p" deur grepe aangedui word 0xD0 0xBF, en die volgende "r" is reeds 0xD1 0x80. In 'n voorvoegselboom lei dit tot die verdeling van die ouernodus in twee - een vir die voorvoegsel 0xD0, en nog een vir 0xD1 (alhoewel die hele Cyrilliese alfabet slegs deur die tweede greep geënkodeer kon word).

Wat het ek gekry

Gekonfronteer met hierdie probleem, het ek besluit om speletjies met stukkies te oefen, en terselfdertyd 'n bietjie beter vertroud te raak met die struktuur van Unicode as 'n geheel. Die resultaat was die UTF-C-enkoderingsformaat ("C" vir compact), wat nie meer as 3 grepe per kodepunt spandeer nie, en jou baie dikwels net toelaat om te spandeer een ekstra greep vir die hele geënkodeerde lyn. Dit lei tot die feit dat sulke enkodering op baie nie-ASCII-alfabette blyk te wees 30-60% meer kompak as UTF-8.

Ek het voorbeelde van implementering van enkoderings- en dekoderingsalgoritmes in die vorm aangebied JavaScript en Go biblioteke, kan jy dit vrylik in jou kode gebruik. Maar ek sal steeds beklemtoon dat hierdie formaat in 'n sekere sin 'n "fiets" bly, en ek beveel nie aan om dit te gebruik nie sonder om te besef hoekom jy dit nodig het. Dit is steeds meer 'n eksperiment as 'n ernstige "verbetering van UTF-8". Nietemin is die kode daar netjies, bondig geskryf met 'n groot aantal opmerkings en toetsdekking.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
Toetsresultate en vergelyking met UTF-8

Ek het ook demo bladsy, waar jy die prestasie van die algoritme kan evalueer, en dan sal ek jou meer vertel oor die beginsels en ontwikkelingsproses daarvan.

Elimineer oortollige stukkies

Ek het natuurlik UTF-8 as basis geneem. Die eerste en mees voor die hand liggende ding wat daarin verander kan word, is om die aantal diensbisse in elke greep te verminder. Byvoorbeeld, die eerste greep in UTF-8 begin altyd met een van die twee 0, of met 11 - 'n voorvoegsel 10 Slegs die volgende grepe het dit. Kom ons vervang die voorvoegsel 11 op 1, en vir die volgende grepe sal ons die voorvoegsels heeltemal verwyder. Wat sal gebeur?

0xxxxxxx — 1 byte
10xxxxxx xxxxxxxx - 2 grepe
110xxxxx xxxxxxxx xxxxxxxx - 3 grepe

Wag, waar is die vier-grepe rekord? Maar dit is nie meer nodig nie - as ons in drie grepe skryf, het ons nou 21 bisse beskikbaar en dit is voldoende vir alle getalle tot 0x10FFFF.

Wat het ons hier opgeoffer? Die belangrikste ding is die opsporing van karaktergrense vanaf 'n arbitrêre plek in die buffer. Ons kan nie na 'n arbitrêre greep wys en die begin van die volgende karakter daaruit vind nie. Dit is 'n beperking van ons formaat, maar in die praktyk is dit selde nodig. Ons kan gewoonlik van die begin af deur die buffer hardloop (veral wanneer dit by kort lyne kom).

Die situasie met die dekking van tale met 2 grepe het ook beter geword: nou gee die twee-grepe-formaat 'n reeks van 14 bisse, en dit is kodes tot 0x3FFF. Die Chinese is ongelukkig (hul karakters wissel meestal van 0x4E00 aan 0x9FFF), maar Georgiërs en baie ander mense het meer pret - hul tale pas ook in 2 grepe per karakter.

Voer die enkodeerderstatus in

Kom ons dink nou oor die eienskappe van die lyne self. Die woordeboek bevat meestal woorde wat in karakters van dieselfde alfabet geskryf is, en dit geld ook vir baie ander tekste. Dit sal goed wees om hierdie alfabet een keer aan te dui, en dan slegs die nommer van die letter daarin aan te dui. Kom ons kyk of die rangskikking van karakters in die Unicode-tabel ons sal help.

Soos hierbo genoem, is Unicode verdeel in vliegtuig 65536 kodes elk. Maar dit is nie 'n baie nuttige verdeling nie (soos reeds gesê, is ons meestal in die nulvlak). Meer interessant is die verdeling deur blokke. Hierdie reekse het nie meer 'n vaste lengte nie, en is meer betekenisvol - as 'n reël kombineer elkeen karakters uit dieselfde alfabet.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
'n Blok wat karakters van die Bengaalse alfabet bevat. Ongelukkig, om historiese redes, is dit 'n voorbeeld van nie baie digte verpakking nie - 96 karakters is chaoties versprei oor 128 blokkodepunte.

Die begin van blokke en hul groottes is altyd veelvoude van 16 - dit word eenvoudig vir gerief gedoen. Daarbenewens begin en eindig baie blokke op waardes wat veelvoude van 128 of selfs 256 is - byvoorbeeld, die basiese Cyrilliese alfabet neem 256 grepe van 0x0400 aan 0x04FF. Dit is baie gerieflik: as ons die voorvoegsel een keer stoor 0x04, dan kan enige Cyrilliese karakter in een greep geskryf word. Dit is waar, op hierdie manier sal ons die geleentheid verloor om terug te keer na ASCII (en na enige ander karakters in die algemeen). Daarom doen ons dit:

  1. Twee grepe 10yyyyyy yxxxxxxx nie net 'n simbool met 'n getal aandui nie yyyyyy yxxxxxxx, maar ook verander huidige alfabet op yyyyyy y0000000 (m.a.w. ons onthou al die stukkies behalwe die minste betekenisvolles 7 bietjie);
  2. Een byte 0xxxxxxx dit is die karakter van die huidige alfabet. Dit moet net bygevoeg word by die offset wat ons in stap 1 onthou het. Alhoewel ons nie die alfabet verander het nie, is die offset nul, so ons het verenigbaarheid met ASCII gehandhaaf.

Net so vir kodes wat 3 grepe benodig:

  1. Drie grepe 110yyyyy yxxxxxxx xxxxxxxx dui 'n simbool met 'n nommer aan yyyyyy yxxxxxxx xxxxxxxx, verander huidige alfabet op yyyyyy y0000000 00000000 (het alles onthou behalwe die jongeres 15 bietjie), en merk die blokkie waarin ons nou is lank modus (wanneer die alfabet terug na 'n dubbelgreep een verander, sal ons hierdie vlag terugstel);
  2. Twee grepe 0xxxxxxx xxxxxxxx in lang modus is dit die karakter van die huidige alfabet. Net so voeg ons dit by met die offset vanaf stap 1. Die enigste verskil is dat ons nou twee grepe lees (omdat ons na hierdie modus oorgeskakel het).

Klink goed: terwyl ons nou karakters uit dieselfde 7-bis Unicode-reeks moet enkodeer, spandeer ons 1 ekstra greep aan die begin en 'n totaal van een greep per karakter.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
Werk vanaf een van die vorige weergawes. Dit klop reeds dikwels UTF-8, maar daar is nog ruimte vir verbetering.

Wat is erger? Eerstens het ons 'n voorwaarde, nl huidige alfabetverskuiwing en merkblokkie lang modus. Dit beperk ons ​​verder: nou kan dieselfde karakters in verskillende kontekste verskillend geënkodeer word. Soek vir substringe, byvoorbeeld, sal gedoen moet word met inagneming hiervan, en nie net deur grepe te vergelyk nie. Tweedens, sodra ons die alfabet verander het, het dit sleg geword met die enkodering van ASCII-karakters (en dit is nie net die Latynse alfabet nie, maar ook basiese leestekens, insluitend spasies) - hulle vereis dat die alfabet weer na 0 verander word, dit wil sê, weer 'n ekstra greep (en dan nog een om terug te kom na ons hoofpunt).

Een alfabet is goed, twee is beter

Kom ons probeer om ons bietjie voorvoegsels 'n bietjie te verander, deur nog een in te druk tot die drie hierbo beskryf:

0xxxxxxx — 1 greep in normale modus, 2 in lang modus
11xxxxxx — 1 byte
100xxxxx xxxxxxxx - 2 grepe
101xxxxx xxxxxxxx xxxxxxxx - 3 grepe

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8

Nou in 'n twee-grepe rekord is daar een minder beskikbare bis - kode wys tot 0x1FFFen nie 0x3FFF. Dit is egter steeds merkbaar groter as in dubbelgreep UTF-8-kodes, mees algemene tale pas steeds in, die mees opvallende verlies het uitgeval hiragana и katakana, die Japannese is hartseer.

Wat is hierdie nuwe kode? 11xxxxxx? Dit is 'n klein "stash" van 64 karakters groot, dit komplementeer ons hoofalfabet, so ek het dit hulp genoem (hulp) alfabet. Wanneer ons die huidige alfabet verander, word 'n stukkie van die ou alfabet bykomstig. Ons het byvoorbeeld van ASCII na Cyrillies oorgeskakel - die stash bevat nou 64 karakters wat Latynse alfabet, getalle, spasie en komma (mees gereelde invoegings in nie-ASCII-tekste). Skakel terug na ASCII - en die hoofdeel van die Cyrilliese alfabet sal die hulpalfabet word.

Danksy toegang tot twee alfabette kan ons 'n groot aantal tekste hanteer met minimale koste om alfabette te verander (leestekens sal meestal lei tot 'n terugkeer na ASCII, maar daarna sal ons baie nie-ASCII karakters van die addisionele alfabet kry, sonder weer oorskakel).

Bonus: die voorvoegsel van die sub-alfabet 11xxxxxx en die keuse van sy aanvanklike verrekening om te wees 0xC0, kry ons gedeeltelike verenigbaarheid met CP1252. Met ander woorde, baie (maar nie alle) Wes-Europese tekste wat in CP1252 geënkodeer is, sal dieselfde lyk in UTF-C.

Hier ontstaan ​​egter 'n moeilikheid: hoe om 'n hulp een uit die hoofalfabet te verkry? Jy kan dieselfde verrekening laat, maar - helaas - hier speel die Unicode-struktuur reeds teen ons. Baie dikwels is die hoofgedeelte van die alfabet nie aan die begin van die blok nie (byvoorbeeld, die Russiese hoofstad "A" het die kode 0x0410, hoewel die Cyrilliese blok begin met 0x0400). Dus, nadat ons die eerste 64 karakters in die stash geneem het, kan ons toegang tot die stertgedeelte van die alfabet verloor.

Om hierdie probleem op te los, het ek met die hand deur 'n paar blokke gegaan wat met verskillende tale ooreenstem, en die afwyking van die hulpalfabet in die hoof-alfabet gespesifiseer. Die Latynse alfabet, as 'n uitsondering, is oor die algemeen herrangskik soos basis64.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8

Finale aanslag

Kom ons dink uiteindelik oor waar anders ons iets kan verbeter.

Let daarop dat die formaat 101xxxxx xxxxxxxx xxxxxxxx laat jou toe om getalle tot en te kodeer 0x1FFFFF, en Unicode eindig vroeër, by 0x10FFFF. Met ander woorde, die laaste kodepunt sal voorgestel word as 10110000 11111111 11111111. Daarom kan ons sê dat as die eerste greep van die vorm is 1011xxxx (Waar xxxx groter as 0), dan beteken dit iets anders. Jy kan byvoorbeeld nog 15 karakters daar byvoeg wat voortdurend beskikbaar is vir enkodering in een greep, maar ek het besluit om dit anders te doen.

Kom ons kyk nou na daardie Unicode-blokke wat drie grepe benodig. Basies, soos reeds genoem, is dit Chinese karakters - maar dit is moeilik om iets daarmee te doen, daar is 21 duisend van hulle. Maar hiragana en katakana het ook soontoe gevlieg – en daar is nie meer so baie van hulle nie, minder as tweehonderd. En sedert ons die Japannese onthou het, is daar ook emoji's (in werklikheid is hulle op baie plekke in Unicode versprei, maar die hoofblokke is in die reeks 0x1F300 - 0x1FBFF). As jy daaraan dink dat daar nou emoji's is wat uit verskeie kodepunte gelyktydig saamgestel word (byvoorbeeld die emoji ‍‍‍Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8 uit soveel as 7 kodes bestaan!), dan word dit 'n groot skande om drie grepe aan elkeen te spandeer (7×3 = 21 grepe ter wille van een ikoon, 'n nagmerrie).

Daarom kies ons 'n paar geselekteerde reekse wat ooreenstem met emoji, hiragana en katakana, hernommer hulle in een deurlopende lys en enkodeer hulle as twee grepe in plaas van drie:

1011xxxx xxxxxxxx

Fantasties: die bogenoemde emojiNog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8, wat uit 7 kodepunte bestaan, neem 8 grepe in UTF-25, en ons pas dit in 14 (presies twee grepe vir elke kodepunt). Terloops, Habr het geweier om dit te verteer (beide in die ou en in die nuwe redakteur), so ek moes dit by 'n prentjie insit.

Kom ons probeer nog een probleem oplos. Soos ons onthou, is die basiese alfabet in wese hoog 6 bisse, wat ons in gedagte hou en aan die kode van elke volgende gedekodeerde simbool plak. In die geval van Chinese karakters wat in die blok is 0x4E00 - 0x9FFF, dit is óf bietjie 0 óf 1. Dit is nie baie gerieflik nie: ons sal voortdurend die alfabet tussen hierdie twee waardes moet wissel (d.w.s. spandeer drie grepe). Maar let daarop dat ons in die lang modus van die kode self die aantal karakters wat ons enkodeer kan aftrek met die kort modus (na al die truuks wat hierbo beskryf is, is dit 10240) - dan sal die reeks hiërogliewe verskuif na 0x2600 - 0x77FF, en in hierdie geval, deur hierdie hele reeks, sal die mees betekenisvolle 6 bisse (uit 21) gelyk wees aan 0. Rye van hiërogliewe sal dus twee grepe per hiërogliewe gebruik (wat optimaal is vir so 'n groot reeks), sonder wat alfabetskakelaars veroorsaak.

Alternatiewe oplossings: SCSU, BOCU-1

Unicode-kundiges, nadat hulle pas die titel van die artikel gelees het, sal u waarskynlik haas om u te herinner dat daar direk onder die Unicode-standaarde is Standaard kompressieskema vir Unicode (SCSU), wat 'n enkoderingsmetode beskryf wat baie soortgelyk is aan dié wat in die artikel beskryf word.

Ek erken eerlik: ek het eers van die bestaan ​​daarvan geleer nadat ek diep gedompel was in die skryf van my besluit. As ek van die begin af daarvan geweet het, sou ek waarskynlik probeer het om 'n implementering te skryf in plaas daarvan om met my eie benadering vorendag te kom.

Wat interessant is, is dat SCSU idees gebruik wat baie soortgelyk is aan dié waarmee ek op my eie vorendag gekom het (in plaas van die konsep van “alfabette” gebruik hulle “vensters”, en daar is meer van hulle beskikbaar as wat ek het). Terselfdertyd het hierdie formaat ook nadele: dit is 'n bietjie nader aan kompressiealgoritmes as koderingsalgoritmes. Die standaard gee veral baie voorstellingsmetodes, maar sê nie hoe om die optimale een te kies nie - hiervoor moet die enkodeerder 'n soort heuristiek gebruik. Dus, 'n SCSU-enkodeerder wat goeie verpakking produseer, sal meer kompleks en omslagtiger wees as my algoritme.

Ter vergelyking het ek 'n relatief eenvoudige implementering van SCSU na JavaScript oorgedra - in terme van kodevolume was dit vergelykbaar met my UTF-C, maar in sommige gevalle was die resultaat tientalle persent erger (soms kan dit dit oorskry, maar nie veel nie). Tekste in Hebreeus en Grieks is byvoorbeeld deur UTF-C geënkodeer 60% beter as SCSU (waarskynlik as gevolg van hul kompakte alfabette).

Afsonderlik sal ek byvoeg dat daar behalwe SCSU ook 'n ander manier is om Unicode kompak voor te stel - BOCU-1, maar dit streef na MIME-versoenbaarheid (wat ek nie nodig gehad het nie) en neem 'n effens ander benadering tot enkodering. Ek het nie die doeltreffendheid daarvan beoordeel nie, maar dit lyk vir my of dit onwaarskynlik is dat dit hoër as SCSU sal wees.

Moontlike verbeterings

Die algoritme wat ek aangebied het, is nie universeel deur ontwerp nie (dit is waarskynlik waar my doelwitte die meeste afwyk van die doelwitte van die Unicode-konsortium). Ek het reeds genoem dat dit hoofsaaklik vir een taak ontwikkel is (berging van 'n meertalige woordeboek in 'n voorvoegselboom), en sommige van sy kenmerke is dalk nie goed geskik vir ander take nie. Maar die feit dat dit nie 'n standaard is nie, kan 'n pluspunt wees - jy kan dit maklik verander om by jou behoeftes te pas.

Byvoorbeeld, op die ooglopende manier kan jy ontslae raak van die teenwoordigheid van staat, maak staatlose kodering - moet net nie veranderlikes opdateer nie offs, auxOffs и is21Bit in die enkodeerder en dekodeerder. In hierdie geval sal dit nie moontlik wees om opeenvolgings van karakters van dieselfde alfabet effektief te pak nie, maar daar sal 'n waarborg wees dat dieselfde karakter altyd met dieselfde grepe geënkodeer is, ongeag die konteks.

Daarbenewens kan jy die enkodeerder aanpas by 'n spesifieke taal deur die verstekstatus te verander - byvoorbeeld, fokus op Russiese tekste, stel die enkodeerder en dekodeerder aan die begin offs = 0x0400 и auxOffs = 0. Dit maak veral sin in die geval van staatlose modus. Oor die algemeen sal dit soortgelyk wees aan die gebruik van die ou agtbis-kodering, maar sonder om die vermoë te verwyder om karakters van alle Unicode in te voeg soos nodig.

Nog 'n nadeel wat vroeër genoem is, is dat daar in groot teks wat in UTF-C geënkodeer is, geen vinnige manier is om die karaktergrens naaste aan 'n arbitrêre greep te vind nie. As jy die laaste, sê maar, 100 grepe van die geënkodeerde buffer afsny, loop jy die risiko om vullis te kry waarmee jy niks kan doen nie. Die enkodering is nie ontwerp vir die stoor van multi-gigagrepe logs nie, maar oor die algemeen kan dit reggestel word. Byte 0xBF moet nooit as die eerste greep verskyn nie (maar kan die tweede of derde wees). Daarom, wanneer u enkodering, kan u die volgorde invoeg 0xBF 0xBF 0xBF elke, sê, 10 KB - dan, as jy 'n grens moet vind, sal dit genoeg wees om die geselekteerde stuk te skandeer totdat 'n soortgelyke merker gevind word. Na aanleiding van die laaste 0xBF is gewaarborg om die begin van 'n karakter te wees. (By dekodering sal hierdie volgorde van drie grepe natuurlik geïgnoreer moet word.)

Opsomming

As jy tot hier gelees het, baie geluk! Ek hoop jy het, soos ek, iets nuuts geleer (of jou geheue verfris) oor die struktuur van Unicode.

Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8
Demo bladsy. Die voorbeeld van Hebreeus toon die voordele bo beide UTF-8 en SCSU.

Die bogenoemde navorsing moet nie as 'n inbreuk op standaarde beskou word nie. Ek is egter oor die algemeen tevrede met die resultate van my werk, so ek is tevrede daarmee te deel: byvoorbeeld, 'n verkleinde JS-biblioteek weeg slegs 1710 grepe (en het natuurlik geen afhanklikhede nie). Soos ek hierbo genoem het, kan haar werk gevind word by demo bladsy (daar is ook 'n stel tekste waarop dit met UTF-8 en SCSU vergelyk kan word).

Ten slotte sal ek weereens die aandag vestig op gevalle waarin UTF-C gebruik word nie die moeite werd nie:

  • As jou reëls lank genoeg is (van 100-200 karakters). In hierdie geval moet u daaraan dink om kompressie-algoritmes soos deflate te gebruik.
  • As jy nodig het ASCII deursigtigheid, dit wil sê, dit is vir jou belangrik dat die geënkodeerde rye nie ASCII-kodes bevat wat nie in die oorspronklike string was nie. Die behoefte hiervoor kan vermy word as jy, wanneer jy met derdeparty-API's interaksie het (byvoorbeeld, met 'n databasis werk), die enkoderingsresultaat as 'n abstrakte stel grepe deurgee, en nie as stringe nie. Andersins loop jy die risiko om onverwagte kwesbaarhede te kry.
  • As jy vinnig karaktergrense teen 'n arbitrêre verskuiwing wil kan vind (byvoorbeeld wanneer 'n deel van 'n lyn beskadig is). Dit kan gedoen word, maar slegs deur die lyn van die begin af te skandeer (of die wysiging toe te pas wat in die vorige afdeling beskryf is).
  • As jy vinnig bewerkings op die inhoud van snare moet uitvoer (sorteer hulle, soek substringe daarin, koppel aan). Dit vereis dat stringe eers gedekodeer word, dus sal UTF-C in hierdie gevalle stadiger as UTF-8 wees (maar vinniger as kompressie-algoritmes). Aangesien dieselfde string altyd op dieselfde manier geënkodeer word, is presiese vergelyking van dekodering nie nodig nie en kan dit op 'n greep-vir-greep-basis gedoen word.

Update: gebruiker Tyomitch in die kommentaar hieronder het 'n grafiek geplaas wat die toepaslikheidsbeperkings van UTF-C beklemtoon. Dit wys dat UTF-C meer doeltreffend is as 'n algemene-doel kompressie-algoritme ('n variasie van LZW) solank die gepakte string korter is ~140 karakters (Ek let egter daarop dat die vergelyking op een teks uitgevoer is; vir ander tale kan die resultaat verskil).
Nog 'n fiets: ons stoor Unicode-stringe 30-60% meer kompak as UTF-8

Bron: will.com

Voeg 'n opmerking