En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8

Om du Àr en utvecklare och stÄr inför uppgiften att vÀlja en kodning, sÄ kommer Unicode nÀstan alltid att vara rÀtt lösning. Den specifika representationsmetoden beror pÄ sammanhanget, men oftast finns det ett universellt svar Àven hÀr - UTF-8. Det som Àr bra med det Àr att det lÄter dig anvÀnda alla Unicode-tecken utan att behöva spendera pengar för mycket mÄnga byte i de flesta fall. Det Àr sant, för sprÄk som anvÀnder mer Àn bara det latinska alfabetet, Àr "inte för mycket" Ätminstone tvÄ byte per tecken. Kan vi bli bÀttre utan att ÄtergÄ till förhistoriska kodningar som begrÀnsar oss till bara 256 tillgÀngliga tecken?

Nedan föreslÄr jag att du ska bekanta dig med mitt försök att svara pÄ denna frÄga och implementera en relativt enkel algoritm som lÄter dig lagra rader pÄ de flesta sprÄk i vÀrlden utan att lÀgga till redundansen som finns i UTF-8.

Varning. Jag kommer omedelbart att göra nÄgra viktiga reservationer: den beskrivna lösningen erbjuds inte som en universell ersÀttning för UTF-8, det Àr bara lÀmpligt i en smal lista av fall (mer om dem nedan), och i inget fall bör det anvÀndas för att interagera med tredje parts API:er (som inte ens vet om det). Oftast Àr komprimeringsalgoritmer för allmÀnna ÀndamÄl (till exempel deflate) lÀmpliga för kompakt lagring av stora volymer textdata. Dessutom, redan i processen med att skapa min lösning, hittade jag en befintlig standard i Unicode sjÀlv, som löser samma problem - det Àr nÄgot mer komplicerat (och ofta vÀrre), men det Àr ÀndÄ en accepterad standard, och inte bara uttryckt tillsammans pÄ knÀet. Jag ska berÀtta om honom ocksÄ.

Om Unicode och UTF-8

Till att börja med nÄgra ord om vad det Àr Unicode О UTF-8.

Som ni vet var 8-bitars kodningar populĂ€ra. Med dem var allt enkelt: 256 tecken kan numreras med siffror frĂ„n 0 till 255, och nummer frĂ„n 0 till 255 kan uppenbarligen representeras som en byte. Om vi ​​gĂ„r tillbaka till början Ă€r ASCII-kodningen helt begrĂ€nsad till 7 bitar, sĂ„ den mest signifikanta biten i dess byte-representation Ă€r noll, och de flesta 8-bitars kodningar Ă€r kompatibla med den (de skiljer sig bara i den "övre" del, dĂ€r den viktigaste biten Ă€r en ).

Hur skiljer sig Unicode frÄn dessa kodningar och varför Àr sÄ mÄnga specifika representationer associerade med det - UTF-8, UTF-16 (BE och LE), UTF-32? LÄt oss reda ut det i ordning.

Den grundlĂ€ggande Unicode-standarden beskriver endast överensstĂ€mmelsen mellan tecken (och i vissa fall individuella komponenter av tecken) och deras nummer. Och det finns mĂ„nga möjliga siffror i denna standard - frĂ„n 0x00 ĐŽĐŸ 0x10FFFF (1 114 112 stycken). Om vi ​​ville sĂ€tta ett tal i ett sĂ„dant intervall i en variabel skulle varken 1 eller 2 byte vara tillrĂ€ckligt för oss. Och eftersom vĂ„ra processorer inte Ă€r sĂ€rskilt designade för att arbeta med tre-byte-nummer, skulle vi tvingas anvĂ€nda sĂ„ mĂ„nga som 4 byte per tecken! Detta Ă€r UTF-32, men det Ă€r just pĂ„ grund av denna "slöseri" som detta format inte Ă€r populĂ€rt.

Lyckligtvis Àr ordningen pÄ tecken i Unicode inte slumpmÀssig. Hela deras set Àr uppdelat i 17"flygplan", som var och en innehÄller 65536 (0x10000) «kodpunkter" Konceptet med en "kodpunkt" hÀr Àr helt enkelt teckennummer, tilldelad till den av Unicode. Men, som nÀmnts ovan, i Unicode Àr inte bara enskilda tecken numrerade, utan ocksÄ deras komponenter och servicemÀrken (och ibland motsvarar ingenting alls numret - kanske för tillfÀllet, men för oss Àr detta inte sÄ viktigt), sÄ det Àr mer korrekt att alltid prata specifikt om antalet siffror i sig, och inte symboler. Men i det följande, för korthetens skull, kommer jag ofta att anvÀnda ordet "symbol", vilket antyder termen "kodpunkt".

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Unicode-plan. Som du kan se Àr det mesta (plan 4 till 13) fortfarande oanvÀnt.

Det som Ă€r mest anmĂ€rkningsvĂ€rt Ă€r att all huvud "massa" ligger i nollplanet, det kallas "GrundlĂ€ggande flersprĂ„kigt plan". Om en rad innehĂ„ller text pĂ„ ett av de moderna sprĂ„ken (inklusive kinesiska) kommer du inte att gĂ„ bortom detta plan. Men du kan inte heller skĂ€ra av resten av Unicode - till exempel Ă€r emoji huvudsakligen placerade i slutet av nĂ€sta plan"Kompletterande flersprĂ„kigt plan"(den strĂ€cker sig frĂ„n 0x10000 ĐŽĐŸ 0x1FFFF). SĂ„ UTF-16 gör detta: alla tecken som faller inom GrundlĂ€ggande flersprĂ„kigt plan, kodas "som de Ă€r" med ett motsvarande tvĂ„bytenummer. Vissa av siffrorna i det hĂ€r intervallet indikerar dock inte specifika tecken alls, men indikerar att efter detta par av byte mĂ„ste vi övervĂ€ga en annan - genom att kombinera vĂ€rdena för dessa fyra byte tillsammans fĂ„r vi ett tal som tĂ€cker hela det giltiga Unicode-intervallet. Denna idĂ© kallas "surrogatpar" - du kanske har hört talas om dem.

SĂ„ UTF-16 krĂ€ver tvĂ„ eller (i mycket sĂ€llsynta fall) fyra byte per "kodpunkt". Detta Ă€r bĂ€ttre Ă€n att anvĂ€nda fyra byte hela tiden, men latin (och andra ASCII-tecken) nĂ€r de kodas pĂ„ detta sĂ€tt slösar bort halva utrymmet pĂ„ nollor. UTF-8 Ă€r utformad för att korrigera detta: ASCII i den upptar, som tidigare, endast en byte; koder frĂ„n 0x80 ĐŽĐŸ 0x7FF - tvĂ„ byte; frĂ„n 0x800 ĐŽĐŸ 0xFFFF - tre, och frĂ„n 0x10000 ĐŽĐŸ 0x10FFFF - fyra. Å ena sidan har det latinska alfabetet blivit bra: kompatibiliteten med ASCII har Ă„tervĂ€nt, och fördelningen Ă€r mer jĂ€mnt "spridd ut" frĂ„n 1 till 4 byte. Men andra alfabet Ă€n latin, tyvĂ€rr, gynnas inte pĂ„ nĂ„got sĂ€tt jĂ€mfört med UTF-16, och mĂ„nga krĂ€ver nu tre byte istĂ€llet för tvĂ„ - intervallet som tĂ€cks av en tvĂ„-byte-post har minskat med 32 gĂ„nger, med 0xFFFF ĐŽĐŸ 0x7FF, och varken kinesiska eller till exempel georgiska ingĂ„r i den. Kyrilliska och fem andra alfabet - hurra - tur, 2 byte per tecken.

Varför hÀnder detta? LÄt oss se hur UTF-8 representerar teckenkoder:
En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Direkt för att representera siffror anvÀnds bitar markerade med symbolen hÀr x. Det kan ses att i en tvÄ-byte-post finns det bara 11 sÄdana bitar (av 16). De ledande bitarna har hÀr endast en hjÀlpfunktion. I fallet med en fyra-byte-post tilldelas 21 av 32 bitar för kodpunktsnumret - det verkar som att tre byte (som ger totalt 24 bitar) skulle vara tillrÀckligt, men servicemarkörer Àter upp för mycket.

Är det hĂ€r dĂ„ligt? Inte riktigt. Å ena sidan, om vi bryr oss mycket om rymden, har vi komprimeringsalgoritmer som enkelt kan eliminera all extra entropi och redundans. Å andra sidan var mĂ„let med Unicode att tillhandahĂ„lla en sĂ„ universell kodning som möjligt. Till exempel kan vi anförtro en rad kodad i UTF-8 till kod som tidigare bara fungerade med ASCII, och inte vara rĂ€dda för att den ska se ett tecken frĂ„n ASCII-intervallet som faktiskt inte finns dĂ€r (trots allt, i UTF-8 alla byte som börjar med frĂ„n nollbiten - det Ă€r precis vad ASCII Ă€r). Och om vi plötsligt vill skĂ€ra av en liten svans frĂ„n en stor strĂ€ng utan att avkoda den frĂ„n första början (eller Ă„terstĂ€lla en del av informationen efter en skadad sektion), Ă€r det lĂ€tt för oss att hitta offset dĂ€r en karaktĂ€r börjar (det rĂ€cker för att hoppa över bytes som har ett bitprefix 10).

Varför dÄ uppfinna nÄgot nytt?

Samtidigt finns det ibland situationer nĂ€r komprimeringsalgoritmer som deflate Ă€r dĂ„ligt tillĂ€mpliga, men du vill uppnĂ„ kompakt lagring av strĂ€ngar. Personligen stötte jag pĂ„ detta problem nĂ€r jag funderade pĂ„ att bygga komprimerat prefixtrĂ€d för en stor ordbok med ord pĂ„ godtyckliga sprĂ„k. Å ena sidan Ă€r varje ord vĂ€ldigt kort, sĂ„ att komprimera det blir ineffektivt. Å andra sidan var trĂ€dimplementeringen som jag ansĂ„g utformad sĂ„ att varje byte av den lagrade strĂ€ngen genererade en separat trĂ€dpunkt, sĂ„ att minimera deras antal var mycket anvĂ€ndbart. I mitt bibliotek Az.js (Som i pymorfi2, pĂ„ vilken den Ă€r baserad) kan ett liknande problem enkelt lösas - strĂ€ngar packade i DAWG-ordbok, lagrad dĂ€r i gamla goda CP1251. Men, som Ă€r lĂ€tt att förstĂ„, fungerar detta bra bara för ett begrĂ€nsat alfabet - en rad pĂ„ kinesiska kan inte lĂ€ggas till i en sĂ„dan ordbok.

Separat skulle jag vilja notera ytterligare en obehaglig nyans som uppstĂ„r nĂ€r man anvĂ€nder UTF-8 i en sĂ„dan datastruktur. Bilden ovan visar att nĂ€r ett tecken skrivs som tvĂ„ byte kommer bitarna som Ă€r relaterade till dess nummer inte i en rad, utan separeras av ett par bitar 10 i mitten: 110xxxxx 10xxxxxx. PĂ„ grund av detta, nĂ€r de lĂ€gre 6 bitarna av den andra byten svĂ€mmar över i teckenkoden (dvs en övergĂ„ng intrĂ€ffar 10111111 → 10000000), sĂ„ Ă€ndras ocksĂ„ den första byten. Det visar sig att bokstaven "p" betecknas med bytes 0xD0 0xBF, och nĂ€sta "r" Ă€r redan 0xD1 0x80. I ett prefixtrĂ€d leder detta till att förĂ€ldranoden delas i tvĂ„ - en för prefixet 0xD0, och en annan för 0xD1 (Ă€ven om hela det kyrilliska alfabetet endast kunde kodas av den andra byten).

Vad fick jag

Inför detta problem bestÀmde jag mig för att öva pÄ att spela spel med bitar, och samtidigt bekanta mig lite bÀttre med strukturen i Unicode som helhet. Resultatet blev UTF-C-kodningsformatet ("C" för kompakt), som inte spenderar mer Àn 3 byte per kodpunkt, och mycket ofta lÄter dig bara spendera en extra byte för hela den kodade raden. Detta leder till det faktum att pÄ mÄnga icke-ASCII-alfabet visar sig sÄdan kodning vara 30-60 % mer kompakt Àn UTF-8.

Jag har presenterat exempel pĂ„ implementering av kodnings- och avkodningsalgoritmer i formulĂ€ret JavaScript och Go-bibliotek, kan du fritt anvĂ€nda dem i din kod. Men jag kommer fortfarande att betona att det hĂ€r formatet pĂ„ ett sĂ€tt förblir en "cykel", och jag rekommenderar inte att du anvĂ€nder det utan att inse varför du behöver det. Detta Ă€r fortfarande mer ett experiment Ă€n en allvarlig "förbĂ€ttring av UTF-8". ÄndĂ„ Ă€r koden dĂ€r skriven snyggt, kortfattat, med ett stort antal kommentarer och testtĂ€ckning.

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Testresultat och jÀmförelse med UTF-8

Det gjorde jag ocksÄ demosida, dÀr du kan utvÀrdera algoritmens prestanda, och sedan kommer jag att berÀtta mer om dess principer och utvecklingsprocess.

Eliminera redundanta bitar

Jag tog UTF-8 som grund sÄklart. Det första och mest uppenbara som kan Àndras i den Àr att minska antalet tjÀnstebitar i varje byte. Till exempel börjar den första byten i UTF-8 alltid med nÄgondera 0, eller med 11 - ett prefix 10 Endast följande byte har det. LÄt oss byta ut prefixet 11 pÄ 1, och för nÀsta byte kommer vi att ta bort prefixen helt. Vad kommer att hÀnda?

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

VÀnta, var Àr posten pÄ fyra byte? Men det behövs inte lÀngre - nÀr man skriver i tre byte har vi nu 21 bitar tillgÀngliga och det rÀcker för alla nummer upp till 0x10FFFF.

Vad har vi offrat hÀr? Det viktigaste Àr upptÀckten av teckengrÀnser frÄn en godtycklig plats i bufferten. Vi kan inte peka pÄ en godtycklig byte och hitta början pÄ nÀsta tecken frÄn den. Detta Àr en begrÀnsning av vÄrt format, men i praktiken Àr detta sÀllan nödvÀndigt. Vi brukar kunna köra igenom bufferten redan frÄn början (sÀrskilt nÀr det gÀller korta linjer).

Situationen med att tĂ€cka sprĂ„k med 2 byte har ocksĂ„ blivit bĂ€ttre: nu ger tvĂ„byteformatet ett intervall pĂ„ 14 bitar, och det Ă€r koder upp till 0x3FFF. Kineserna har otur (deras karaktĂ€rer strĂ€cker sig mestadels frĂ„n 0x4E00 ĐŽĐŸ 0x9FFF), men georgier och mĂ„nga andra folk har roligare - deras sprĂ„k passar ocksĂ„ in i 2 byte per tecken.

Ange kodartillstÄndet

LÄt oss nu tÀnka pÄ egenskaperna hos sjÀlva linjerna. Ordboken innehÄller oftast ord skrivna med bokstÀver av samma alfabet, och det gÀller Àven mÄnga andra texter. Det skulle vara bra att ange detta alfabet en gÄng och sedan endast ange numret pÄ bokstaven i det. LÄt oss se om arrangemanget av tecken i Unicode-tabellen kommer att hjÀlpa oss.

Som nÀmnts ovan Àr Unicode indelad i plan 65536 koder vardera. Men detta Àr inte en mycket anvÀndbar uppdelning (som redan sagt, oftast Àr vi i nollplanet). Mer intressant Àr uppdelningen efter block. Dessa intervall har inte lÀngre en fast lÀngd och Àr mer meningsfulla - som regel kombinerar vart och ett av tecken frÄn samma alfabet.

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Ett block som innehÄller tecken frÄn det bengaliska alfabetet. TyvÀrr, av historiska skÀl, Àr detta ett exempel pÄ inte sÀrskilt tÀt förpackning - 96 tecken Àr kaotiskt utspridda över 128 blockkodpunkter.

Början av block och deras storlekar Ă€r alltid multiplar av 16 - detta görs helt enkelt för bekvĂ€mlighet. Dessutom börjar och slutar mĂ„nga block pĂ„ vĂ€rden som Ă€r multiplar av 128 eller till och med 256 - till exempel tar det grundlĂ€ggande kyrilliska alfabetet upp 256 byte frĂ„n 0x0400 ĐŽĐŸ 0x04FF. Detta Ă€r ganska bekvĂ€mt: om vi sparar prefixet en gĂ„ng 0x04, dĂ„ kan vilket kyrilliskt tecken som helst skrivas i en byte. Det Ă€r sant att vi pĂ„ detta sĂ€tt förlorar möjligheten att Ă„tervĂ€nda till ASCII (och till alla andra karaktĂ€rer i allmĂ€nhet). DĂ€rför gör vi sĂ„ hĂ€r:

  1. TvÄ byte 10yyyyyy yxxxxxxx inte bara beteckna en symbol med ett nummer yyyyyy yxxxxxxx, men ocksÄ förÀndras nuvarande alfabetet pÄ yyyyyy y0000000 (dvs vi kommer ihÄg alla bitar utom de minst signifikanta 7-bit);
  2. En byte 0xxxxxxx detta Ă€r karaktĂ€ren för det aktuella alfabetet. Det behöver bara lĂ€ggas till förskjutningen som vi kom ihĂ„g i steg 1. Även om vi inte Ă€ndrade alfabetet Ă€r förskjutningen noll, sĂ„ vi bibehöll kompatibilitet med ASCII.

LikasÄ för koder som krÀver 3 byte:

  1. Tre byte 110yyyyy yxxxxxxx xxxxxxxx ange en symbol med ett nummer yyyyyy yxxxxxxx xxxxxxxx, förÀndra nuvarande alfabetet pÄ yyyyyy y0000000 00000000 (kom ihÄg allt utom de yngre 15-bit), och markera rutan som vi nu Àr i lÄng lÀge (nÀr vi Àndrar alfabetet tillbaka till ett dubbelbyte, ÄterstÀller vi denna flagga);
  2. TvÄ byte 0xxxxxxx xxxxxxxx i lÄngt lÀge Àr det karaktÀren för det aktuella alfabetet. PÄ samma sÀtt lÀgger vi till det med offset frÄn steg 1. Den enda skillnaden Àr att nu lÀser vi tvÄ byte (eftersom vi bytte till detta lÀge).

LÄter bra: medan vi nu behöver koda tecken frÄn samma 7-bitars Unicode-intervall, spenderar vi 1 extra byte i början och totalt en byte per tecken.

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Arbetar frÄn en av de tidigare versionerna. Den slÄr redan ofta UTF-8, men det finns fortfarande utrymme för förbÀttringar.

Vad Àr vÀrre? För det första har vi ett villkor, nÀmligen aktuell alfabetsoffset och kryssrutan lÄngt lÀge. Detta begrÀnsar oss ytterligare: nu kan samma tecken kodas olika i olika sammanhang. Att söka efter delstrÀngar, till exempel, mÄste göras med hÀnsyn till detta, och inte bara genom att jÀmföra bytes. För det andra, sÄ snart vi Àndrade alfabetet, blev det dÄligt med kodningen av ASCII-tecken (och detta Àr inte bara det latinska alfabetet, utan ocksÄ grundlÀggande interpunktion, inklusive mellanslag) - de krÀver att alfabetet Àndras igen till 0, det vill sÀga, Äterigen en extra byte (och sedan ytterligare en för att komma tillbaka till vÄr huvudpoÀng).

Ett alfabet Àr bra, tvÄ Àr bÀttre

LÄt oss försöka Àndra vÄra bitprefix lite och klÀmma in ett till till de tre som beskrivs ovan:

0xxxxxxx — 1 byte i normalt lĂ€ge, 2 i lĂ„ngt lĂ€ge
11xxxxxx — 1 byte
100xxxxx xxxxxxxx - 2 byte
101xxxxx xxxxxxxx xxxxxxxx - 3 byte

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8

Nu i en tvÄ-byte-post finns det en mindre tillgÀnglig bit - kodpunkter upp till 0x1FFFOch inte 0x3FFF. Det Àr dock fortfarande mÀrkbart större Àn i dubbelbyte UTF-8-koder, de vanligaste sprÄken passar fortfarande in, den mest mÀrkbara förlusten har fallit ut hiragana О katakana, japanerna Àr ledsna.

Vad Àr denna nya kod? 11xxxxxx? Det hÀr Àr ett litet "stash" med 64 tecken i storlek, det kompletterar vÄrt huvudalfabet, sÄ jag kallade det extra (extra) alfabetet. NÀr vi byter det nuvarande alfabetet blir en del av det gamla alfabetet extra. Till exempel bytte vi frÄn ASCII till kyrilliska - stash innehÄller nu 64 tecken som innehÄller Latinska alfabetet, siffror, mellanslag och kommatecken (mest frekventa infogningar i icke-ASCII-texter). Byt tillbaka till ASCII - och huvuddelen av det kyrilliska alfabetet kommer att bli hjÀlpalfabetet.

Tack vare tillgÄng till tvÄ alfabet kan vi hantera ett stort antal texter med minimala kostnader för att byta alfabet (interpunktion leder oftast till en ÄtergÄng till ASCII, men efter det kommer vi att fÄ mÄnga icke-ASCII-tecken frÄn tillÀggsalfabetet, utan byter igen).

Bonus: prefixet underalfabetet 11xxxxxx och vÀlja att dess initiala offset ska vara 0xC0, fÄr vi delvis kompatibilitet med CP1252. Med andra ord kommer mÄnga (men inte alla) vÀsteuropeiska texter kodade i CP1252 att se likadana ut i UTF-C.

HÀr uppstÄr dock en svÄrighet: hur fÄr man en extra frÄn huvudalfabetet? Du kan lÀmna samma offset, men - tyvÀrr - hÀr spelar Unicode-strukturen redan mot oss. Mycket ofta Àr huvuddelen av alfabetet inte i början av blocket (till exempel den ryska huvudstaden "A" har koden 0x0410, Àven om det kyrilliska blocket börjar med 0x0400). Efter att ha tagit de första 64 tecknen i arkivet kan vi förlora tillgÄngen till alfabetets bakdel.

För att ÄtgÀrda det hÀr problemet gick jag manuellt igenom nÄgra block som motsvarar olika sprÄk och specificerade förskjutningen av hjÀlpalfabetet inom det huvudsakliga alfabetet för dem. Det latinska alfabetet, som ett undantag, omordnades i allmÀnhet som base64.

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8

Sista handen

LÄt oss Àntligen fundera pÄ var vi annars kan förbÀttra nÄgot.

Observera att formatet 101xxxxx xxxxxxxx xxxxxxxx lÄter dig koda nummer upp till 0x1FFFFF, och Unicode slutar tidigare, kl 0x10FFFF. Med andra ord kommer den sista kodpunkten att representeras som 10110000 11111111 11111111. DÀrför kan vi sÀga att om den första byten Àr av formen 1011xxxx (Var xxxx större Àn 0), betyder det nÄgot annat. Till exempel kan du lÀgga till ytterligare 15 tecken dÀr som stÀndigt Àr tillgÀngliga för kodning i en byte, men jag bestÀmde mig för att göra det annorlunda.

LĂ„t oss titta pĂ„ de Unicode-block som krĂ€ver tre byte nu. I grund och botten, som redan nĂ€mnts, Ă€r dessa kinesiska tecken - men det Ă€r svĂ„rt att göra nĂ„got med dem, det finns 21 tusen av dem. Men Ă€ven hiragana och katakana flög dit – och det finns inte sĂ„ mĂ„nga av dem lĂ€ngre, mindre Ă€n tvĂ„hundra. Och eftersom vi kom ihĂ„g japanerna, finns det ocksĂ„ emojis (i sjĂ€lva verket Ă€r de utspridda pĂ„ mĂ„nga stĂ€llen i Unicode, men huvudblocken finns i sortimentet 0x1F300 - 0x1FBFF). Om du tĂ€nker pĂ„ det faktum att det nu finns emojis som Ă€r sammansatta frĂ„n flera kodpunkter samtidigt (till exempel emojin ‍‍‍En annan cykel: vi lagrar Unicode-strĂ€ngar 30-60% mer kompakt Ă€n UTF-8 bestĂ„r av sĂ„ mĂ„nga som 7 koder!), dĂ„ blir det synd att lĂ€gga tre byte pĂ„ varje (7×3 = 21 byte för en ikons skull, en mardröm).

DÀrför vÀljer vi nÄgra utvalda intervall som motsvarar emoji, hiragana och katakana, numrerar om dem till en kontinuerlig lista och kodar dem som tvÄ byte istÀllet för tre:

1011xxxx xxxxxxxx

Bra: den ovannÀmnda emojinEn annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8, som bestÄr av 7 kodpunkter, tar 8 byte i UTF-25, och vi passar in den 14 (exakt tvÄ byte för varje kodpunkt). Habr vÀgrade förresten att smÀlta det (bÄde i den gamla och i den nya editorn), sÄ jag var tvungen att lÀgga in den med en bild.

LÄt oss försöka lösa ett problem till. Som vi minns Àr det grundlÀggande alfabetet i huvudsak hög 6 bitar, som vi har i Ätanke och limmar till koden för varje nÀsta avkodade symbol. NÀr det gÀller kinesiska tecken som finns i blocket 0x4E00 - 0x9FFF, detta Àr antingen bit 0 eller 1. Detta Àr inte sÀrskilt bekvÀmt: vi kommer att behöva stÀndigt byta alfabetet mellan dessa tvÄ vÀrden (dvs spendera tre byte). Men observera att i det lÄnga lÀget, frÄn sjÀlva koden kan vi subtrahera antalet tecken som vi kodar med det korta lÀget (efter alla knep som beskrivs ovan Àr detta 10240) - dÄ kommer hieroglyfernas intervall att skifta till 0x2600 - 0x77FF, och i det hÀr fallet, genom hela detta intervall, kommer de mest signifikanta 6 bitarna (av 21) att vara lika med 0. SÄledes kommer sekvenser av hieroglyfer att anvÀnda tvÄ byte per hieroglyf (vilket Àr optimalt för ett sÄ stort intervall), utan orsakar alfabetsvÀxlar.

Alternativa lösningar: SCSU, BOCU-1

Unicode-experter, efter att precis ha lÀst artikelns titel, kommer sannolikt att skynda sig att pÄminna dig om att direkt bland Unicode-standarderna finns Standardkompressionsschema för Unicode (SCSU), som beskriver en kodningsmetod mycket lik den som beskrivs i artikeln.

Jag erkĂ€nner Ă€rligt: ​​jag fick veta om dess existens först efter att jag var djupt fördjupad i att skriva mitt beslut. Hade jag vetat om det frĂ„n början sĂ„ hade jag nog försökt skriva en implementering istĂ€llet för att komma pĂ„ ett eget tillvĂ€gagĂ„ngssĂ€tt.

Vad som Àr intressant Àr att SCSU anvÀnder idéer som liknar de jag kom pÄ sjÀlv (istÀllet för begreppet "alfabet" anvÀnder de "fönster", och det finns fler tillgÀngliga Àn jag har). Samtidigt har detta format ocksÄ nackdelar: det Àr lite nÀrmare komprimeringsalgoritmer Àn kodningsalgoritmer. I synnerhet ger standarden mÄnga representationsmetoder, men sÀger inte hur man vÀljer den optimala - för detta mÄste kodaren anvÀnda nÄgon form av heuristik. SÄledes kommer en SCSU-kodare som producerar bra förpackningar att vara mer komplex och krÄngligare Àn min algoritm.

Som jÀmförelse överförde jag en relativt enkel implementering av SCSU till JavaScript - vad gÀller kodvolym visade den sig vara jÀmförbar med min UTF-C, men i vissa fall blev resultatet tiotals procent sÀmre (ibland kan det överskrida det, men inte mycket). Till exempel kodades texter pÄ hebreiska och grekiska av UTF-C 60 % bÀttre Àn SCSU (förmodligen pÄ grund av deras kompakta alfabet).

Separat tillÀgger jag att det förutom SCSU ocksÄ finns ett annat sÀtt att kompakt representera Unicode - BOCU-1, men det syftar till MIME-kompatibilitet (vilket jag inte behövde) och tar ett lite annorlunda tillvÀgagÄngssÀtt för kodning. Jag har inte bedömt dess effektivitet, men det förefaller mig som om det Àr osannolikt att det Àr högre Àn SCSU.

Möjliga förbÀttringar

Algoritmen jag presenterade Àr inte universell till sin design (det Àr förmodligen dÀr mina mÄl avviker mest frÄn Unicode-konsortiets mÄl). Jag har redan nÀmnt att den utvecklades frÀmst för en uppgift (lagring av en flersprÄkig ordbok i ett prefixtrÀd), och vissa av dess funktioner kanske inte Àr vÀl lÀmpade för andra uppgifter. Men det faktum att det inte Àr en standard kan vara ett plus - du kan enkelt modifiera den för att passa dina behov.

Till exempel, pÄ det uppenbara sÀttet kan du bli av med nÀrvaron av tillstÄnd, göra tillstÄndslös kodning - bara uppdatera inte variabler offs, auxOffs О is21Bit i kodaren och avkodaren. I det hÀr fallet kommer det inte att vara möjligt att effektivt packa sekvenser av tecken i samma alfabet, men det kommer att finnas en garanti för att samma tecken alltid Àr kodat med samma byte, oavsett sammanhang.

Dessutom kan du skrÀddarsy kodaren till ett specifikt sprÄk genom att Àndra standardtillstÄndet - till exempel fokusera pÄ ryska texter, stÀll in kodaren och avkodaren i början offs = 0x0400 О auxOffs = 0. Detta Àr sÀrskilt vettigt i fallet med statslöst lÀge. I allmÀnhet kommer detta att likna att anvÀnda den gamla Ättabitarskodningen, men utan att ta bort möjligheten att infoga tecken frÄn all Unicode efter behov.

En annan nackdel som nÀmnts tidigare Àr att det i stor text kodad i UTF-C inte finns nÄgot snabbt sÀtt att hitta teckengrÀnsen nÀrmast en godtycklig byte. Om du skÀr av de sista, sÀg, 100 byten frÄn den kodade bufferten, riskerar du att fÄ skrÀp som du inte kan göra nÄgot med. Kodningen Àr inte designad för att lagra loggar pÄ flera gigabyte, men i allmÀnhet kan detta korrigeras. Byte 0xBF fÄr aldrig visas som den första byten (men kan vara den andra eller tredje). DÀrför kan du infoga sekvensen nÀr du kodar 0xBF 0xBF 0xBF var, sÀg, 10 KB - dÄ, om du behöver hitta en grÀns, rÀcker det med att skanna den valda biten tills en liknande markör hittas. Efter det sista 0xBF Àr garanterat början pÄ en karaktÀr. (Vid avkodning kommer denna sekvens pÄ tre byte naturligtvis att behöva ignoreras.)

Sammanfattningsvis

Om du har lÀst sÄ hÀr lÄngt, grattis! Jag hoppas att du, precis som jag, har lÀrt dig nÄgot nytt (eller frÀschat upp ditt minne) om Unicodes struktur.

En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8
Demosida. Exemplet med hebreiska visar fördelarna jÀmfört med bÄde UTF-8 och SCSU.

Den ovan beskrivna forskningen ska inte betraktas som ett intrÄng i standarder. Jag Àr dock generellt sett nöjd med resultatet av mitt arbete, sÄ jag Àr nöjd med dem del: till exempel vÀger ett minifierat JS-bibliotek bara 1710 byte (och har naturligtvis inga beroenden). Som jag nÀmnde ovan finns hennes arbete pÄ demosida (det finns ocksÄ en uppsÀttning texter dÀr den kan jÀmföras med UTF-8 och SCSU).

Slutligen ska jag Äterigen uppmÀrksamma fall dÀr UTF-C anvÀnds inte vÀrt det:

  • Om dina rader Ă€r tillrĂ€ckligt lĂ„nga (frĂ„n 100-200 tecken). I det hĂ€r fallet bör du tĂ€nka pĂ„ att anvĂ€nda komprimeringsalgoritmer som deflate.
  • Om du behöver ASCII-transparens, det vill sĂ€ga det Ă€r viktigt för dig att de kodade sekvenserna inte innehĂ„ller ASCII-koder som inte fanns i den ursprungliga strĂ€ngen. Behovet av detta kan undvikas om du, nĂ€r du interagerar med tredje parts API:er (till exempel arbetar med en databas), skickar kodningsresultatet som en abstrakt uppsĂ€ttning byte, och inte som strĂ€ngar. Annars riskerar du att fĂ„ ovĂ€ntade sĂ„rbarheter.
  • Om du snabbt vill kunna hitta teckengrĂ€nser vid en godtycklig offset (till exempel nĂ€r en del av en linje Ă€r skadad). Detta kan göras, men bara genom att skanna linjen frĂ„n början (eller tillĂ€mpa Ă€ndringen som beskrivs i föregĂ„ende avsnitt).
  • Om du snabbt behöver utföra operationer pĂ„ innehĂ„llet i strĂ€ngar (sortera dem, sök efter delstrĂ€ngar i dem, sammanfoga). Detta krĂ€ver att strĂ€ngar avkodas först, sĂ„ UTF-C kommer att vara lĂ„ngsammare Ă€n UTF-8 i dessa fall (men snabbare Ă€n komprimeringsalgoritmer). Eftersom samma strĂ€ng alltid Ă€r kodad pĂ„ samma sĂ€tt, krĂ€vs ingen exakt jĂ€mförelse av avkodning och kan göras byte-för-byte.

Uppdatering: AnvÀndaren Tyomitch i kommentarerna nedan publicerade en graf som belyser tillÀmplighetsgrÀnserna för UTF-C. Det visar att UTF-C Àr effektivare Àn en allmÀn komprimeringsalgoritm (en variant av LZW) sÄ lÀnge den packade strÀngen Àr kortare ~140 tecken (Jag noterar dock att jÀmförelsen utfördes pÄ en text; för andra sprÄk kan resultatet skilja sig).
En annan cykel: vi lagrar Unicode-strÀngar 30-60% mer kompakt Àn UTF-8

KĂ€lla: will.com

LĂ€gg en kommentar