En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8

Hvis du er en utvikler og står overfor oppgaven med å velge en koding, så vil Unicode nesten alltid være den rette løsningen. Den spesifikke representasjonsmetoden avhenger av konteksten, men som oftest finnes det et universelt svar også her - UTF-8. Det som er bra med det er at det lar deg bruke alle Unicode-tegn uten å bruke penger for mye mange byte i de fleste tilfeller. Sant nok, for språk som bruker mer enn bare det latinske alfabetet, er "ikke for mye" i det minste to byte per tegn. Kan vi gjøre det bedre uten å gå tilbake til forhistoriske kodinger som begrenser oss til bare 256 tilgjengelige tegn?

Nedenfor foreslår jeg å gjøre deg kjent med mitt forsøk på å svare på dette spørsmålet og implementere en relativt enkel algoritme som lar deg lagre linjer på de fleste språk i verden uten å legge til redundansen som er i UTF-8.

Ansvarsfraskrivelse. Jeg vil umiddelbart ta noen viktige forbehold: den beskrevne løsningen tilbys ikke som en universell erstatning for UTF-8, det er bare egnet i en smal liste over tilfeller (mer om dem nedenfor), og bør ikke i noe tilfelle brukes til å samhandle med tredjeparts APIer (som ikke engang vet om det). Oftest er generelle komprimeringsalgoritmer (for eksempel deflater) egnet for kompakt lagring av store mengder tekstdata. I tillegg, allerede i prosessen med å lage løsningen min, fant jeg en eksisterende standard i selve Unicode, som løser det samme problemet - det er noe mer komplisert (og ofte verre), men det er likevel en akseptert standard, og ikke bare satt sammen på kneet. Jeg skal fortelle deg om ham også.

Om Unicode og UTF-8

Til å begynne med, noen få ord om hva det er Unicode и UTF-8.

Som du vet, pleide 8-bits koding å være populært. Med dem var alt enkelt: 256 tegn kan nummereres med tall fra 0 til 255, og tall fra 0 til 255 kan åpenbart representeres som én byte. Hvis vi går tilbake til begynnelsen, er ASCII-kodingen fullstendig begrenset til 7 biter, så den mest signifikante biten i byte-representasjonen er null, og de fleste 8-bits kodinger er kompatible med den (de skiller seg bare i den "øvre" del, der den viktigste biten er en ).

Hvordan skiller Unicode seg fra disse kodingene og hvorfor er så mange spesifikke representasjoner knyttet til det - UTF-8, UTF-16 (BE og LE), UTF-32? La oss ordne det i rekkefølge.

Den grunnleggende Unicode-standarden beskriver bare korrespondansen mellom tegn (og i noen tilfeller individuelle komponenter av tegn) og tallene deres. Og det er mange mulige tall i denne standarden - fra 0x00 til 0x10FFFF (1 stykker). Hvis vi ønsket å sette et tall i et slikt område inn i en variabel, ville verken 114 eller 112 byte vært nok for oss. Og siden våre prosessorer ikke er spesielt designet for å jobbe med tre-byte tall, ville vi bli tvunget til å bruke så mange som 1 byte per tegn! Dette er UTF-2, men det er nettopp på grunn av denne "sløsheten" at dette formatet ikke er populært.

Heldigvis er ikke rekkefølgen på tegn i Unicode tilfeldig. Hele settet deres er delt inn i 17"fly", som hver inneholder 65536 (0x10000) «kodepunkter" Konseptet med et "kodepunkt" her er ganske enkelt tegnnummer, tildelt den av Unicode. Men som nevnt ovenfor, i Unicode er ikke bare individuelle tegn nummerert, men også deres komponenter og tjenestemerker (og noen ganger tilsvarer ingenting i det hele tatt nummeret - kanskje foreløpig, men for oss er dette ikke så viktig), så det er mer riktig å alltid snakke spesifikt om selve antallet tall, og ikke symboler. Imidlertid vil jeg i det følgende, for korthets skyld, ofte bruke ordet "symbol", som antyder begrepet "kodepunkt".

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
Unicode-fly. Som du kan se er det meste (fly 4 til 13) fortsatt ubrukt.

Det som er mest bemerkelsesverdig er at all hoved "massen" ligger i nullplanet, det kalles "Grunnleggende flerspråklig plan". Hvis en linje inneholder tekst på et av de moderne språkene (inkludert kinesisk), vil du ikke gå utover dette planet. Men du kan heller ikke kutte av resten av Unicode - for eksempel er emoji hovedsakelig plassert på slutten av neste fly,"Supplerende flerspråklig plan"(det strekker seg fra 0x10000 til 0x1FFFF). Så UTF-16 gjør dette: alle karakterer som faller innenfor Grunnleggende flerspråklig plan, er kodet "som de er" med et tilsvarende to-byte nummer. Noen av tallene i dette området indikerer imidlertid ikke spesifikke tegn i det hele tatt, men indikerer at etter dette paret med byte må vi vurdere en annen - ved å kombinere verdiene til disse fire bytene sammen, får vi et tall som dekker hele det gyldige Unicode-området. Denne ideen kalles "surrogatpar" - du har kanskje hørt om dem.

Så UTF-16 krever to eller (i svært sjeldne tilfeller) fire byte per "kodepunkt". Dette er bedre enn å bruke fire byte hele tiden, men latin (og andre ASCII-tegn) når kodet på denne måten kaster bort halvparten av plassen på nuller. UTF-8 er designet for å rette opp dette: ASCII i den opptar, som før, bare én byte; koder fra 0x80 til 0x7FF - to byte; fra 0x800 til 0xFFFF - tre, og fra 0x10000 til 0x10FFFF - fire. På den ene siden har det latinske alfabetet blitt bra: kompatibiliteten med ASCII har kommet tilbake, og fordelingen er jevnere "spredt ut" fra 1 til 4 byte. Men andre alfabeter enn latin, dessverre, er ikke gunstige på noen måte sammenlignet med UTF-16, og mange krever nå tre byte i stedet for to - rekkevidden som dekkes av en to-byte-post har blitt redusert med 32 ganger, med 0xFFFF til 0x7FF, og verken kinesisk eller for eksempel georgisk er inkludert i den. Kyrilliske og fem andre alfabeter - hurra - heldig, 2 byte per tegn.

Hvorfor skjer dette? La oss se hvordan UTF-8 representerer tegnkoder:
En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
Direkte for å representere tall, brukes biter merket med symbolet her x. Det kan sees at i en to-byte-post er det bare 11 slike biter (av 16). De ledende bitene har her bare en hjelpefunksjon. Når det gjelder en fire-byte-post, er 21 av 32 biter tildelt kodepunktnummeret – det ser ut til at tre byte (som gir totalt 24 biter) ville være nok, men tjenestemarkører spiser for mye.

Er dette dårlig? Ikke egentlig. På den ene siden, hvis vi bryr oss mye om plass, har vi kompresjonsalgoritmer som enkelt kan eliminere all den ekstra entropien og redundansen. På den annen side var målet med Unicode å gi en mest mulig universell koding. For eksempel kan vi overlate en linje kodet i UTF-8 til kode som tidligere bare fungerte med ASCII, og ikke være redd for at den vil se et tegn fra ASCII-området som faktisk ikke er der (tross alt, i UTF-8 alt bytes som starter med fra nullbiten - dette er nøyaktig hva ASCII er). Og hvis vi plutselig ønsker å kutte av en liten hale fra en stor streng uten å dekode den helt fra begynnelsen (eller gjenopprette deler av informasjonen etter en skadet seksjon), er det lett for oss å finne forskyvningen der en karakter begynner (det er nok for å hoppe over byte som har et bit-prefiks 10).

Hvorfor da finne på noe nytt?

Samtidig er det noen ganger situasjoner der komprimeringsalgoritmer som deflate er dårlig anvendelige, men du ønsker å oppnå kompakt lagring av strenger. Personlig møtte jeg dette problemet når jeg tenkte på å bygge komprimert prefiksetre for en stor ordbok som inkluderer ord på vilkårlige språk. På den ene siden er hvert ord veldig kort, så å komprimere det vil være ineffektivt. På den annen side var treimplementeringen som jeg vurderte designet slik at hver byte av den lagrede strengen genererte et separat trevertex, så det var veldig nyttig å minimere antallet. I biblioteket mitt Az.js (Som i pymorfi2, som den er basert på) kan et lignende problem enkelt løses - strenger pakket inn i DAWG-ordbok, lagret der i gode gamle CP1251. Men, som det er lett å forstå, fungerer dette bra bare for et begrenset alfabet - en linje på kinesisk kan ikke legges til i en slik ordbok.

Separat vil jeg notere en mer ubehagelig nyanse som oppstår når du bruker UTF-8 i en slik datastruktur. Bildet ovenfor viser at når et tegn skrives som to byte, kommer ikke bitene relatert til dets nummer på rad, men er atskilt med et par biter 10 i midten: 110xxxxx 10xxxxxx. På grunn av dette, når de nedre 6 bitene av den andre byten flyter over i tegnkoden (dvs. en overgang skjer 1011111110000000), så endres også den første byten. Det viser seg at bokstaven "p" er angitt med byte 0xD0 0xBF, og neste "r" er allerede 0xD1 0x80. I et prefiksetre fører dette til at den overordnede noden deles i to - en for prefikset 0xD0, og en annen for 0xD1 (selv om hele det kyrilliske alfabetet bare kunne kodes av den andre byten).

Hva fikk jeg

Stilt overfor dette problemet bestemte jeg meg for å trene på å spille spill med bits, og samtidig bli litt bedre kjent med strukturen til Unicode som helhet. Resultatet var UTF-C-kodingsformatet ("C" for kompakt), som bruker ikke mer enn 3 byte per kodepunkt, og veldig ofte lar deg bruke bare én ekstra byte for hele den kodede linjen. Dette fører til det faktum at slik koding viser seg å være på mange ikke-ASCII-alfabeter 30-60 % mer kompakt enn UTF-8.

Jeg har presentert eksempler på implementering av kodings- og dekodingsalgoritmer i skjemaet JavaScript og Go-biblioteker, kan du fritt bruke dem i koden din. Men jeg vil likevel understreke at på en måte forblir dette formatet en "sykkel", og jeg anbefaler ikke å bruke det uten å skjønne hvorfor du trenger det. Dette er fortsatt mer et eksperiment enn en seriøs «forbedring av UTF-8». Likevel er koden der skrevet pent, konsist, med et stort antall kommentarer og testdekning.

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
Testresultater og sammenligning med UTF-8

Det gjorde jeg også demoside, hvor du kan evaluere ytelsen til algoritmen, og så vil jeg fortelle deg mer om prinsippene og utviklingsprosessen.

Eliminerer overflødige biter

Jeg tok selvfølgelig UTF-8 som grunnlag. Den første og mest åpenbare tingen som kan endres i den er å redusere antall tjenestebiter i hver byte. For eksempel starter den første byten i UTF-8 alltid med enten 0, eller med 11 - et prefiks 10 Bare følgende byte har det. La oss erstatte prefikset 111, og for de neste bytene vil vi fjerne prefiksene fullstendig. Hva vil skje?

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

Vent, hvor er firebyte-posten? Men det er ikke lenger nødvendig - når vi skriver i tre byte har vi nå 21 bits tilgjengelig og dette er tilstrekkelig for alle tall opp til 0x10FFFF.

Hva har vi ofret her? Det viktigste er deteksjon av tegngrenser fra en vilkårlig plassering i bufferen. Vi kan ikke peke på en vilkårlig byte og finne begynnelsen av neste tegn fra den. Dette er en begrensning av formatet vårt, men i praksis er dette sjelden nødvendig. Vi er vanligvis i stand til å kjøre gjennom bufferen helt fra begynnelsen (spesielt når det gjelder korte linjer).

Situasjonen med å dekke språk med 2 byte har også blitt bedre: nå gir to-byte-formatet en rekkevidde på 14 biter, og dette er koder opp til 0x3FFF. Kineserne er uheldige (karakterene deres varierer stort sett fra 0x4E00 til 0x9FFF), men georgiere og mange andre folk har mer moro - språkene deres passer også inn i 2 byte per tegn.

Gå inn i kodertilstanden

La oss nå tenke på egenskapene til selve linjene. Ordboken inneholder oftest ord skrevet med tegn i samme alfabet, og dette gjelder også for mange andre tekster. Det ville være greit å angi dette alfabetet én gang, og deretter angi bare nummeret på bokstaven i det. La oss se om arrangementet av tegn i Unicode-tabellen vil hjelpe oss.

Som nevnt ovenfor er Unicode delt inn i flyet 65536 koder hver. Men dette er ikke en veldig nyttig divisjon (som allerede sagt, oftest er vi i nullplanet). Mer interessant er inndelingen etter blokker. Disse områdene har ikke lenger en fast lengde, og er mer meningsfulle - som regel kombinerer hver tegn fra samme alfabet.

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
En blokk som inneholder tegn fra det bengalske alfabetet. Dessverre, av historiske årsaker, er dette et eksempel på ikke veldig tett emballasje - 96 tegn er kaotisk spredt over 128 blokkkodepunkter.

Begynnelsen av blokker og deres størrelser er alltid multipler av 16 - dette gjøres ganske enkelt for enkelhets skyld. I tillegg begynner og slutter mange blokker på verdier som er multipler av 128 eller til og med 256 - for eksempel tar det grunnleggende kyrilliske alfabetet opp 256 byte fra 0x0400 til 0x04FF. Dette er ganske praktisk: hvis vi lagrer prefikset en gang 0x04, så kan et hvilket som helst kyrillisk tegn skrives i én byte. Riktignok vil vi på denne måten miste muligheten til å gå tilbake til ASCII (og til andre karakterer generelt). Derfor gjør vi dette:

  1. To byte 10yyyyyy yxxxxxxx ikke bare betegne et symbol med et tall yyyyyy yxxxxxxx, men også endre gjeldende alfabetyyyyyy y0000000 (dvs. vi husker alle bitene unntatt de minst betydningsfulle 7 bit);
  2. En byte 0xxxxxxx dette er karakteren til det gjeldende alfabetet. Det må bare legges til forskyvningen som vi husket i trinn 1. Selv om vi ikke endret alfabetet, er forskyvningen null, så vi opprettholdt kompatibilitet med ASCII.

På samme måte for koder som krever 3 byte:

  1. Tre byte 110yyyyy yxxxxxxx xxxxxxxx angi et symbol med et tall yyyyyy yxxxxxxx xxxxxxxx, endring gjeldende alfabetyyyyyy y0000000 00000000 (husket alt bortsett fra de yngre 15 bit), og merk av i boksen som vi nå er i lang modus (når du endrer alfabetet tilbake til et dobbeltbyte, vil vi tilbakestille dette flagget);
  2. To byte 0xxxxxxx xxxxxxxx i lang modus er det tegnet til det gjeldende alfabetet. På samme måte legger vi det til med offset fra trinn 1. Den eneste forskjellen er at nå leser vi to byte (fordi vi byttet til denne modusen).

Høres bra ut: nå, mens vi trenger å kode tegn fra det samme 7-bits Unicode-området, bruker vi 1 ekstra byte i begynnelsen og totalt en byte per tegn.

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
Arbeider fra en av de tidligere versjonene. Den slår allerede ofte UTF-8, men det er fortsatt rom for forbedring.

Hva er verre? For det første har vi en betingelse, nemlig gjeldende alfabetforskyvning og avkrysningsboks lang modus. Dette begrenser oss ytterligere: nå kan de samme tegnene kodes forskjellig i forskjellige sammenhenger. Søking etter understrenger, for eksempel, vil måtte gjøres under hensyntagen til dette, og ikke bare ved å sammenligne byte. For det andre, så snart vi endret alfabetet, ble det dårlig med kodingen av ASCII-tegn (og dette er ikke bare det latinske alfabetet, men også grunnleggende tegnsetting, inkludert mellomrom) - de krever å endre alfabetet igjen til 0, det vil si, igjen en ekstra byte (og så en til for å komme tilbake til hovedpoenget vårt).

Ett alfabet er bra, to er bedre

La oss prøve å endre bitprefiksene våre litt, og klemme inn ett til av de tre som er beskrevet ovenfor:

0xxxxxxx — 1 byte i normal modus, 2 i lang modus
11xxxxxx — 1 byte
100xxxxx xxxxxxxx - 2 byte
101xxxxx xxxxxxxx xxxxxxxx - 3 byte

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8

Nå i en to-byte post er det en mindre tilgjengelig bit - kode peker opp til 0x1FFFOg ikke 0x3FFF. Imidlertid er det fortsatt merkbart større enn i dobbelbyte UTF-8-koder, de fleste vanlige språkene passer fortsatt inn, det mest merkbare tapet har falt ut hiragana и katakana, japanerne er triste.

Hva er denne nye koden? 11xxxxxx? Dette er en liten "stash" på 64 tegn i størrelse, den utfyller hovedalfabetet vårt, så jeg kalte det hjelpe (hjelpe) alfabetet. Når vi bytter gjeldende alfabet, blir en del av det gamle alfabetet hjelpe. For eksempel byttet vi fra ASCII til kyrillisk - stashen inneholder nå 64 tegn som inneholder Latinsk alfabet, tall, mellomrom og komma (hyppigste innsettinger i ikke-ASCII-tekster). Bytt tilbake til ASCII - og hoveddelen av det kyrilliske alfabetet vil bli hjelpealfabetet.

Takket være tilgang til to alfabeter kan vi håndtere et stort antall tekster med minimale kostnader for å bytte alfabet (tegnsetting vil oftest føre til tilbakeføring til ASCII, men etter det vil vi få mange ikke-ASCII-tegn fra tilleggsalfabetet, uten bytter igjen).

Bonus: prefikser underalfabetet 11xxxxxx og velge den opprinnelige forskyvningen å være 0xC0, får vi delvis kompatibilitet med CP1252. Med andre ord vil mange (men ikke alle) vesteuropeiske tekster kodet i CP1252 se like ut i UTF-C.

Her oppstår det imidlertid en vanskelighet: hvordan får man tak i et hjelpealfabet fra hovedalfabetet? Du kan la den samme forskyvningen være, men - dessverre - her spiller Unicode-strukturen allerede mot oss. Svært ofte er ikke hoveddelen av alfabetet i begynnelsen av blokken (for eksempel har den russiske hovedstaden "A" koden 0x0410, selv om den kyrilliske blokken begynner med 0x0400). Etter å ha tatt de første 64 tegnene inn i oppbevaringen, kan vi dermed miste tilgangen til haledelen av alfabetet.

For å fikse dette problemet, gikk jeg manuelt gjennom noen blokker som tilsvarer forskjellige språk, og spesifiserte forskyvningen av hjelpealfabetet i hovedalfabetet for dem. Det latinske alfabetet, som et unntak, ble generelt omorganisert som base64.

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8

Siste finpuss

La oss endelig tenke på hvor ellers vi kan forbedre noe.

Merk at formatet 101xxxxx xxxxxxxx xxxxxxxx lar deg kode tall opp til 0x1FFFFF, og Unicode slutter tidligere, kl 0x10FFFF. Med andre ord vil det siste kodepunktet bli representert som 10110000 11111111 11111111. Derfor kan vi si at hvis den første byten er av formen 1011xxxx (hvor xxxx større enn 0), så betyr det noe annet. For eksempel kan du legge til ytterligere 15 tegn der som er konstant tilgjengelige for koding i en byte, men jeg bestemte meg for å gjøre det annerledes.

La oss se på de Unicode-blokkene som krever tre byte nå. I utgangspunktet, som allerede nevnt, er dette kinesiske tegn - men det er vanskelig å gjøre noe med dem, det er 21 tusen av dem. Men hiragana og katakana fløy også dit – og det er ikke så mange av dem lenger, mindre enn to hundre. Og siden vi husket japanerne, er det også emojier (faktisk er de spredt mange steder i Unicode, men hovedblokkene er i området 0x1F300 - 0x1FBFF). Hvis du tenker på det faktum at det nå er emojier som er satt sammen fra flere kodepunkter samtidig (for eksempel emojien ‍‍‍En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8 består av hele 7 koder!), da blir det en skam å bruke tre byte på hver (7×3 = 21 byte for ett ikons skyld, et mareritt).

Derfor velger vi noen få utvalgte områder som tilsvarer emoji, hiragana og katakana, omnummererer dem til en kontinuerlig liste og koder dem som to byte i stedet for tre:

1011xxxx xxxxxxxx

Flott: den nevnte ‍‍‍ emojiEn annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8, som består av 7 kodepunkter, tar 8 byte i UTF-25, og vi passer det inn 14 (nøyaktig to byte for hvert kodepunkt). Habr nektet forresten å fordøye den (både i den gamle og i den nye editoren), så jeg måtte sette den inn med et bilde.

La oss prøve å fikse ett problem til. Som vi husker, er det grunnleggende alfabetet i hovedsak høy 6 bits, som vi husker og limer til koden til hvert neste dekodede symbol. Når det gjelder kinesiske tegn som er i blokken 0x4E00 - 0x9FFF, dette er enten bit 0 eller 1. Dette er ikke veldig praktisk: vi må hele tiden bytte alfabetet mellom disse to verdiene (dvs. bruke tre byte). Men merk at i den lange modusen, fra selve koden, kan vi trekke fra antall tegn som vi koder ved å bruke den korte modusen (etter alle triksene beskrevet ovenfor, er dette 10240) - da vil utvalget av hieroglyfer skifte til 0x2600 - 0x77FF, og i dette tilfellet, gjennom hele dette området, vil de mest signifikante 6 bitene (av 21) være lik 0. Dermed vil sekvenser av hieroglyfer bruke to byte per hieroglyf (som er optimalt for et så stort område), uten forårsaker alfabetbrytere.

Alternative løsninger: SCSU, BOCU-1

Unicode-eksperter, etter å ha lest tittelen på artikkelen, vil mest sannsynlig skynde seg å minne deg på at det er direkte blant Unicode-standardene Standard komprimeringsskjema for Unicode (SCSU), som beskriver en kodingsmetode som er veldig lik den som er beskrevet i artikkelen.

Jeg innrømmer ærlig: Jeg lærte om dens eksistens først etter at jeg var dypt fordypet i å skrive avgjørelsen min. Hadde jeg visst om det fra starten, ville jeg sannsynligvis ha prøvd å skrive en implementering i stedet for å komme med min egen tilnærming.

Det som er interessant er at SCSU bruker ideer som ligner veldig på de jeg kom opp med på egen hånd (i stedet for konseptet "alfabeter" bruker de "vinduer", og det er flere tilgjengelige enn jeg har). Samtidig har dette formatet også ulemper: det er litt nærmere komprimeringsalgoritmer enn kodingsalgoritmer. Spesielt gir standarden mange representasjonsmetoder, men sier ikke hvordan man velger den optimale - for dette må koderen bruke en slags heuristikk. Dermed vil en SCSU-koder som produserer god emballasje være mer kompleks og mer tungvint enn min algoritme.

Til sammenligning overførte jeg en relativt enkel implementering av SCSU til JavaScript - når det gjelder kodevolum viste det seg å være sammenlignbart med min UTF-C, men i noen tilfeller ble resultatet titalls prosent dårligere (noen ganger kan det overskride det, men ikke mye). For eksempel ble tekster på hebraisk og gresk kodet av UTF-C 60 % bedre enn SCSU (sannsynligvis på grunn av deres kompakte alfabeter).

Separat vil jeg legge til at foruten SCSU er det også en annen måte å kompakt representere Unicode - BOCU-1, men den tar sikte på MIME-kompatibilitet (som jeg ikke trengte) og tar en litt annen tilnærming til koding. Jeg har ikke vurdert effektiviteten, men det ser ut til at det neppe er høyere enn SCSU.

Mulige forbedringer

Algoritmen jeg presenterte er ikke universell av design (det er sannsynligvis der målene mine avviker mest fra målene til Unicode Consortium). Jeg har allerede nevnt at den først og fremst ble utviklet for én oppgave (lagring av en flerspråklig ordbok i et prefikstre), og noen av funksjonene er kanskje ikke godt egnet for andre oppgaver. Men det faktum at det ikke er en standard kan være et pluss - du kan enkelt endre den for å passe dine behov.

For eksempel, på den åpenbare måten kan du bli kvitt tilstedeværelsen av stat, lage statsløs koding - bare ikke oppdater variabler offs, auxOffs и is21Bit i koderen og dekoderen. I dette tilfellet vil det ikke være mulig å effektivt pakke sekvenser av tegn i samme alfabet, men det vil være en garanti for at det samme tegnet alltid er kodet med de samme bytene, uavhengig av konteksten.

I tillegg kan du skreddersy koderen til et spesifikt språk ved å endre standardtilstanden - for eksempel fokusere på russiske tekster, stille inn koderen og dekoderen i begynnelsen offs = 0x0400 и auxOffs = 0. Dette er spesielt fornuftig i tilfelle av statsløs modus. Generelt vil dette være likt å bruke den gamle åtte-biters kodingen, men uten å fjerne muligheten til å sette inn tegn fra all Unicode etter behov.

En annen ulempe nevnt tidligere er at i stor tekst kodet i UTF-C er det ingen rask måte å finne tegngrensen nærmest en vilkårlig byte. Hvis du avskjærer de siste, for eksempel, 100 bytene fra den kodede bufferen, risikerer du å få søppel du ikke kan gjøre noe med. Kodingen er ikke laget for lagring av multi-gigabyte logger, men generelt kan dette korrigeres. Byte 0xBF må aldri vises som den første byten (men kan være den andre eller tredje). Derfor, når du koder, kan du sette inn sekvensen 0xBF 0xBF 0xBF hver, si, 10 KB - så, hvis du trenger å finne en grense, vil det være nok å skanne det valgte stykket til en lignende markør er funnet. Etter det siste 0xBF er garantert begynnelsen på en karakter. (Ved dekoding må denne sekvensen på tre byte selvfølgelig ignoreres.)

Oppsummering

Hvis du har lest så langt, gratulerer! Jeg håper du, som meg, har lært noe nytt (eller frisket opp hukommelsen) om strukturen til Unicode.

En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8
Demoside. Eksemplet med hebraisk viser fordelene i forhold til både UTF-8 og SCSU.

Den ovenfor beskrevne forskningen bør ikke betraktes som et inngrep i standarder. Imidlertid er jeg generelt fornøyd med resultatene av arbeidet mitt, så jeg er fornøyd med dem del: for eksempel veier et minifisert JS-bibliotek bare 1710 byte (og har selvfølgelig ingen avhengigheter). Som jeg nevnte ovenfor, kan du finne hennes arbeid på demoside (det er også et sett med tekster som det kan sammenlignes med UTF-8 og SCSU på).

Til slutt vil jeg igjen trekke oppmerksomhet til tilfeller der UTF-C brukes ikke verdt det:

  • Hvis linjene dine er lange nok (fra 100-200 tegn). I dette tilfellet bør du tenke på å bruke komprimeringsalgoritmer som deflate.
  • Hvis du trenger ASCII-gjennomsiktighet, det vil si at det er viktig for deg at de kodede sekvensene ikke inneholder ASCII-koder som ikke var i den opprinnelige strengen. Behovet for dette kan unngås hvis du, når du samhandler med tredjeparts APIer (for eksempel arbeider med en database), sender kodingsresultatet som et abstrakt sett med byte, og ikke som strenger. Ellers risikerer du å få uventede sårbarheter.
  • Hvis du raskt vil være i stand til å finne tegngrenser ved en vilkårlig forskyvning (for eksempel når en del av en linje er skadet). Dette kan gjøres, men bare ved å skanne linjen fra begynnelsen (eller bruke modifikasjonen beskrevet i forrige avsnitt).
  • Hvis du raskt trenger å utføre operasjoner på innholdet i strenger (sortér dem, søk etter understrenger i dem, sett sammen). Dette krever at strenger dekodes først, så UTF-C vil være tregere enn UTF-8 i disse tilfellene (men raskere enn komprimeringsalgoritmer). Siden den samme strengen alltid er kodet på samme måte, er nøyaktig sammenligning av dekoding ikke nødvendig og kan gjøres på en byte-for-byte-basis.

Oppdatering: bruker Tyomitch i kommentarfeltet nedenfor la ut en graf som fremhever bruksgrensene for UTF-C. Den viser at UTF-C er mer effektiv enn en generell komprimeringsalgoritme (en variant av LZW) så lenge den pakkede strengen er kortere ~140 tegn (Jeg bemerker imidlertid at sammenligningen ble utført på én tekst; for andre språk kan resultatet variere).
En annen sykkel: vi lagrer Unicode-strenger 30-60 % mer kompakt enn UTF-8

Kilde: www.habr.com

Legg til en kommentar