Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8

Kui olete arendaja ja seisate silmitsi ülesandega valida kodeering, on Unicode peaaegu alati õige lahendus. Konkreetne esitusviis sõltub kontekstist, kuid enamasti on ka siin universaalne vastus - UTF-8. Selle hea asi on see, et see võimaldab teil kasutada kõiki Unicode'i märke ilma kulutusteta liiga palju enamikul juhtudel palju baite. Tõsi, keeltes, mis kasutavad rohkem kui ainult ladina tähestikku, on "mitte liiga palju" vähemalt kaks baiti tähemärgi kohta. Kas saame paremini hakkama ilma naasmata eelajaloolise kodeeringu juurde, mis piirab meid ainult 256 tähemärgini?

Allpool teen ettepaneku tutvuda minu katsega sellele küsimusele vastata ja rakendada suhteliselt lihtsat algoritmi, mis võimaldab salvestada ridu enamikus maailma keeltes ilma UTF-8 liiasust lisamata.

Vastutusest loobumine. Teen kohe paar olulist reservatsiooni: kirjeldatud lahendust ei pakuta UTF-8 universaalse asendusena, sobib see ainult kitsas loetelus juhtudest (nende kohta lähemalt allpool) ja mitte mingil juhul ei tohiks seda kasutada kolmandate osapoolte API-dega suhtlemiseks (kes sellest isegi ei tea). Suuremahuliste tekstiandmete kompaktseks salvestamiseks sobivad enamasti üldotstarbelised tihendusalgoritmid (näiteks tühjendamine). Lisaks leidsin juba oma lahenduse loomise käigus Unicode'is endas olemasoleva standardi, mis lahendab sama probleemi - see on mõnevõrra keerulisem (ja sageli halvem), kuid siiski on see aktsepteeritud standard, mitte lihtsalt pandud. koos põlve peal. Ma räägin teile ka temast.

Unicode'i ja UTF-8 kohta

Alustuseks paar sõna selle kohta, mis see on Unikood и UTF-8.

Nagu teate, olid 8-bitised kodeeringud populaarsed. Nendega oli kõik lihtne: 256 tähemärki saab nummerdada numbritega 0 kuni 255 ja numbreid 0 kuni 255 saab ilmselgelt esitada ühe baidina. Kui läheme tagasi päris algusesse, on ASCII kodeering täielikult piiratud 7 bitiga, seega on selle baitide esituses kõige olulisem bitt null ja enamik 8-bitiseid kodeeringuid on sellega ühilduvad (need erinevad ainult "ülemise" poolest). osa, kus kõige olulisem bit on üks ).

Mille poolest Unicode nendest kodeeringutest erineb ja miks on sellega seotud nii palju spetsiifilisi esitusi – UTF-8, UTF-16 (BE ja LE), UTF-32? Sorteerime selle järjekorras.

Unicode'i põhistandard kirjeldab ainult märkide (ja mõnel juhul ka märkide üksikute komponentide) ja nende numbrite vahelist vastavust. Ja selles standardis on palju võimalikke numbreid - alates 0x00 kuni 0x10FFFF (1 114 112 tükki). Kui me tahaksime panna muutujasse sellises vahemikus olevat numbrit, ei piisaks meile ei 1 ega 2 baidist. Ja kuna meie protsessorid pole eriti loodud kolmebaidiste numbritega töötamiseks, oleksime sunnitud kasutama koguni 4 baiti tähemärgi kohta! See on UTF-32, kuid just selle "raiskamise" tõttu pole see formaat populaarne.

Õnneks pole Unicode'i tähemärkide järjekord juhuslik. Nende kogu komplekt on jagatud 17"lennukid", millest igaüks sisaldab 65536 (0x10000) «koodipunktid" "Koodipunkti" mõiste on siin lihtne märgi number, mis on sellele Unicode'i poolt määratud. Kuid nagu eespool mainitud, nummerdatakse Unicode'is mitte ainult üksikud märgid, vaid ka nende komponendid ja teenindusmärgid (ja mõnikord ei vasta numbrile mitte midagi - võib-olla esialgu, kuid meie jaoks pole see nii oluline), nii et õigem on alati rääkida konkreetselt numbrite arvust, mitte sümbolitest. Järgnevalt kasutan aga lühiduse huvides sageli sõna “sümbol”, mis viitab terminile “koodipunkt”.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Unicode lennukid. Nagu näha, on suurem osa (lennukid 4 kuni 13) siiani kasutamata.

Kõige tähelepanuväärsem on see, et kogu peamine "pulp" asub nulltasandil, seda nimetatakse "Põhiline mitmekeelne lennuk". Kui rida sisaldab teksti mõnes kaasaegses keeles (sh hiina keeles), ei lähe te sellest tasapinnast kaugemale. Kuid te ei saa ka ülejäänud Unicode'i ära lõigata - näiteks emotikonid asuvad peamiselt keele lõpus järgmine lennuk"Täiendav mitmekeelne lennuk"(see ulatub alates 0x10000 kuni 0x1FFFF). Seega teeb UTF-16 seda: kõik märgid, mis jäävad sellesse Põhiline mitmekeelne lennuk, on kodeeritud "nagu on" vastava kahebaidise numbriga. Mõned selle vahemiku numbrid ei tähista aga üldse konkreetseid märke, vaid näitavad, et pärast seda baitide paari peame arvestama veel ühega – nende nelja baitide väärtused kokku kombineerides saame arvu, mis katab kogu kehtiv Unicode'i vahemik. Seda ideed nimetatakse "asenduspaarideks" - võib-olla olete neist kuulnud.

Seega nõuab UTF-16 kahte või (väga harvadel juhtudel) nelja baiti "koodipunkti" kohta. See on parem kui nelja baiti pidev kasutamine, kuid ladina keel (ja muud ASCII-märgid) raiskab sellisel viisil kodeerituna poole ruumi nullidest. UTF-8 on loodud selle parandamiseks: selles sisalduv ASCII võtab nagu varemgi ainult ühe baidi; koodid alates 0x80 kuni 0x7FF - kaks baiti; alates 0x800 kuni 0xFFFF - kolm ja alates 0x10000 kuni 0x10FFFF - neli. Ühest küljest on ladina tähestik muutunud heaks: ühilduvus ASCII-ga on taastunud ja jaotus on ühtlasemalt "hajutatud" 1–4 baiti. Kuid peale ladina tähestiku ei ole UTF-16-ga võrreldes paraku mingit kasu ja paljud nõuavad nüüd kahe baiti asemel kolme – kahebaidise kirjega hõlmatud vahemik on vähenenud 32 korda. 0xFFFF kuni 0x7FF, ja sinna ei kuulu ei hiina ega näiteks gruusia keel. Kirillitsa ja viis muud tähestikku - hurraa - õnne, 2 baiti tähemärgi kohta.

Miks see juhtub? Vaatame, kuidas UTF-8 tähistab märgikoode:
Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Otse numbrite esitamiseks kasutatakse siin sümboliga tähistatud bitte x. On näha, et kahebaidises kirjes on selliseid bitte ainult 11 (16-st). Juhtbittidel on siin ainult abifunktsioon. Neljabaidise kirje puhul eraldatakse koodipunkti numbri jaoks 21 bitti 32-st - näib, et kolmest baidist (mis annavad kokku 24 bitti) piisaks, kuid teenusemarkerid söövad liiga palju.

Kas see on halb? Mitte päris. Ühest küljest, kui me ruumist palju hoolime, on meil tihendusalgoritmid, mis võivad hõlpsasti kõrvaldada kogu täiendava entroopia ja liiasuse. Teisest küljest oli Unicode'i eesmärk pakkuda võimalikult universaalset kodeerimist. Näiteks võime usaldada UTF-8-ga kodeeritud rea koodile, mis varem töötas ainult ASCII-ga, ja mitte karta, et see näeb ASCII-vahemikus olevat märki, mida seal tegelikult pole (UTF-8-s on ju kõik baite, mis algavad nullbitist – see on täpselt see ASCII). Ja kui me järsku tahame suurelt stringilt väikese saba ära lõigata ilma seda algusest peale dekodeerimata (või taastada osa teabest pärast kahjustatud lõiku), on meil lihtne leida nihe, kust märk algab (piisab biti prefiksiga baitide vahelejätmiseks 10).

Miks siis midagi uut välja mõelda?

Samal ajal on aeg-ajalt olukordi, kus tihendusalgoritmid, nagu deflate, on halvasti rakendatavad, kuid soovite saavutada stringide kompaktse salvestamise. Isiklikult puutusin selle probleemiga kokku ehitamisele mõeldes tihendatud eesliite puu suure sõnastiku jaoks, mis sisaldab sõnu suvalistes keeltes. Ühest küljest on iga sõna väga lühike, nii et selle tihendamine on ebaefektiivne. Teisest küljest oli minu käsitletud puuteostus kavandatud nii, et salvestatud stringi iga bait genereeris eraldi puu tipu, nii et nende arvu minimeerimine oli väga kasulik. Minu raamatukogus Az.js (Nagu pümorfia2, millel see põhineb) sarnase probleemi saab lahendada lihtsalt – stringid sisse pakitud DAWG-sõnastik, sinna salvestatud vana hea CP1251. Kuid nagu on lihtne mõista, töötab see hästi ainult piiratud tähestiku puhul - hiinakeelset rida ei saa sellisesse sõnastikku lisada.

Eraldi tahaksin märkida veel ühe ebameeldiva nüansi, mis tekib UTF-8 kasutamisel sellises andmestruktuuris. Ülaltoodud pildil on näha, et kui märk on kirjutatud kahe baidina, siis selle numbriga seotud bitid ei tule järjest, vaid on eraldatud bitipaariga 10 keskel: 110xxxxx 10xxxxxx. Seetõttu, kui teise baidi alumised 6 bitti täituvad märgikoodis (st toimub üleminek 1011111110000000), siis muutub ka esimene bait. Selgub, et tähte “p” tähistatakse baitidega 0xD0 0xBF, ja järgmine "r" on juba olemas 0xD1 0x80. Eesliidete puus viib see emasõlme jagamiseni kaheks – üheks prefiksi jaoks 0xD0, ja teine ​​jaoks 0xD1 (kuigi kogu kirillitsa tähestikku sai kodeerida ainult teine ​​bait).

Mida ma sain

Selle probleemiga silmitsi seistes otsustasin harjutada bittide mängimist ja samal ajal tutvuda Unicode'i kui terviku struktuuriga. Tulemuseks oli UTF-C kodeeringuvorming ("C" jaoks kompaktne), mis ei kuluta rohkem kui 3 baiti koodipunkti kohta ja võimaldab väga sageli kulutada ainult üks lisabait kogu kodeeritud rea jaoks. See toob kaasa asjaolu, et paljudes mitte-ASCII tähestikes osutub selline kodeering 30–60% kompaktsem kui UTF-8.

Olen esitanud kujul näiteid kodeerimis- ja dekodeerimisalgoritmide rakendamisest JavaScripti ja Go teegid, saate neid oma koodis vabalt kasutada. Kuid rõhutan siiski, et teatud mõttes jääb see formaat “jalgrattaks” ja ma ei soovita seda kasutada mõistmata, miks sa seda vajad. See on ikkagi rohkem eksperiment kui tõsine "UTF-8 täiustamine". Sellegipoolest on sealne kood kirjutatud korralikult, lühidalt, rohkete kommentaaride ja testide kajastusega.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Testi tulemused ja võrdlus UTF-8-ga

tegin ka demo leht, kus saab hinnata algoritmi toimivust ja siis räägin lähemalt selle põhimõtetest ja arendusprotsessist.

Üleliigsete bittide kõrvaldamine

Võtsin muidugi aluseks UTF-8. Esimene ja kõige ilmsem asi, mida selles muuta saab, on teenindusbittide arvu vähendamine igas baidis. Näiteks UTF-8 esimene bait algab alati kummaga 0või koos 11 - eesliide 10 Ainult järgmistel baitidel on see. Asendame eesliide 11 edasi 1, ja järgmiste baitide puhul eemaldame eesliited täielikult. Mis juhtub?

0xxxxxxx - 1 bait
10xxxxxx xxxxxxxx - 2 baiti
110xxxxx xxxxxxxx xxxxxxxx - 3 baiti

Oot, kus on neljabaidine kirje? Kuid seda pole enam vaja – kolme baidiga kirjutades on meil nüüd saadaval 21 bitti ja sellest piisab kõikidele numbritele kuni 0x10FFFF.

Mida me siin ohverdanud oleme? Kõige olulisem on märgipiiride tuvastamine suvalisest asukohast puhvris. Me ei saa osutada suvalisele baidile ja leida sellest järgmise märgi algust. See on meie vormingu piirang, kuid praktikas on see harva vajalik. Tavaliselt suudame puhvri algusest peale läbi joosta (eriti kui tegemist on lühikeste joontega).

Paremaks on muutunud ka olukord 2-baidiliste keelte katmisel: nüüd annab kahebaidine formaat vahemikku 14 bitti ja need on koodid kuni 0x3FFF. Hiinlastel pole õnne (nende tähemärgid ulatuvad enamasti 0x4E00 kuni 0x9FFF), kuid grusiinidel ja paljudel teistel rahvastel on lõbusam – ka nende keeled mahuvad 2 baiti tähemärgi kohta.

Sisestage kodeerija olek

Mõelgem nüüd joonte endi omadustele. Sõnastik sisaldab kõige sagedamini sama tähestiku tähtedega kirjutatud sõnu ja see kehtib ka paljude teiste tekstide kohta. Hea oleks see tähestik üks kord ära märkida ja seejärel ainult selles oleva tähe number. Vaatame, kas märkide paigutus Unicode'i tabelis meid aitab.

Nagu eespool mainitud, on Unicode jagatud lennuk Igaüks 65536 koodi. Kuid see pole eriti kasulik jaotus (nagu juba öeldud, oleme enamasti nulltasandil). Huvitavam on jagamine plokid. Nendel vahemikel ei ole enam kindlat pikkust ja need on tähendusrikkamad – reeglina ühendab igaüks sama tähestiku tähemärke.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Plokk, mis sisaldab bengali tähestiku tähemärke. Kahjuks on see ajaloolistel põhjustel näide mitte väga tihedast pakendist – 96 tähemärki on 128 plokikoodipunktis kaootiliselt hajutatud.

Plokkide algused ja nende suurused on alati 16-kordsed – seda tehakse lihtsalt mugavuse huvides. Lisaks algavad ja lõpevad paljud plokid väärtustega, mis on 128 või isegi 256 kordsed – näiteks võtab põhikirillitsa tähestik enda alla 256 baiti 0x0400 kuni 0x04FF. See on üsna mugav: kui salvestame prefiksi üks kord 0x04, siis saab ühe baidina kirjutada mis tahes kirillitsa tähemärgi. Tõsi, nii kaotame võimaluse naasta ASCII-sse (ja üldse muude tegelaste juurde). Seetõttu teeme järgmist:

  1. Kaks baiti 10yyyyyy yxxxxxxx mitte ainult tähistada sümbolit numbriga yyyyyy yxxxxxxx, vaid ka muuta praegune tähestik edasi yyyyyy y0000000 (st me mäletame kõiki bitte, välja arvatud kõige vähem olulised 7 bit);
  2. Üks bait 0xxxxxxx see on praeguse tähestiku märk. See tuleb lihtsalt lisada 1. sammus meelde jäänud nihkele. Kuigi me tähestikku ei muutnud, on nihe null, seega säilitasime ühilduvuse ASCII-ga.

Samamoodi 3 baiti nõudvate koodide puhul:

  1. Kolm baiti 110yyyyy yxxxxxxx xxxxxxxx tähistage sümbolit numbriga yyyyyy yxxxxxxx xxxxxxxx, muuta praegune tähestik edasi yyyyyy y0000000 00000000 (mäletas kõike, välja arvatud nooremad 15 bit) ja märkige ruut, milles oleme praegu pikk režiim (tähestiku muutmisel tagasi kahebaidiseks lähtestame selle lipu);
  2. Kaks baiti 0xxxxxxx xxxxxxxx pikas režiimis on see praeguse tähestiku märk. Samamoodi lisame selle nihkega sammust 1. Ainus erinevus on see, et nüüd loeme kaks baiti (kuna lülitusime sellele režiimile).

Kõlab hästi: kui nüüd peame kodeerima tähemärke samast 7-bitisest Unicode'i vahemikust, kulutame alguses 1 lisabaidi ja kokku ühe baidi märgi kohta.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Töötab ühest varasemast versioonist. See ületab juba sageli UTF-8, kuid arenguruumi on veel.

Mis on hullem? Esiteks on meil tingimus, nimelt praeguse tähestiku nihe ja märkeruut pikk režiim. See piirab meid veelgi: nüüd saab samu märke erinevates kontekstides erinevalt kodeerida. Näiteks alamstringide otsimisel tuleb seda arvesse võtta, mitte ainult baite võrrelda. Teiseks, niipea kui tähestikku muutsime, muutus see ASCII-märkide kodeerimisega halvaks (ja see pole mitte ainult ladina tähestik, vaid ka põhilised kirjavahemärgid, sealhulgas tühikud) - need nõuavad tähestiku muutmist uuesti 0-ks, see tähendab, jälle lisabait (ja siis veel üks, et jõuda tagasi meie põhipunkti juurde).

Üks tähestik on hea, kaks on parem

Proovime natuke muuta oma biti eesliiteid, pigistades kolme eelkirjeldatud juurde veel ühe:

0xxxxxxx — 1 bait tavarežiimis, 2 pikas režiimis
11xxxxxx - 1 bait
100xxxxx xxxxxxxx - 2 baiti
101xxxxx xxxxxxxx xxxxxxxx - 3 baiti

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8

Nüüd on kahebaidises kirjes saadaval üks bitt vähem – kood osutab kuni 0x1FFFJa mitte 0x3FFF. Kuid see on endiselt märgatavalt suurem kui kahebaidilistes UTF-8 koodides, enamik levinumaid keeli mahub endiselt sisse, kõige märgatavam kadu on välja kukkunud hiragana и katakana, jaapanlased on kurvad.

Mis see uus kood on? 11xxxxxx? See on väike 64 tähemärgi suurune "varjuk", see täiendab meie peamist tähestikku, nii et ma nimetasin seda abistavaks (abistav) tähestik. Kui vahetame praegust tähestikku, muutub osa vanast tähestikust abistavaks. Näiteks lülitusime ASCII-lt kirillitsasse – varras on nüüd 64 tähemärki, mis sisaldavad Ladina tähestik, numbrid, tühik ja koma (kõige sagedasemad sisestused mitte-ASCII tekstides). Lülitage tagasi ASCII-le - ja kirillitsa tähestiku põhiosa muutub abitähestikuks.

Tänu ligipääsule kahele tähestikule saame hakkama suure hulga tekstidega minimaalsete kuludega tähestiku vahetamiseks (kirjavahemärgid viivad enamasti tagasi ASCII juurde, kuid pärast seda saame lisatähestikust palju mitte-ASCII märke, ilma uuesti vahetamine).

Boonus: alamtähestiku eesliide 11xxxxxx ja valides selle esialgseks nihkeks 0xC0, saame osalise ühilduvuse CP1252-ga. Teisisõnu, paljud (kuid mitte kõik) Lääne-Euroopa tekstid, mis on kodeeritud koodiga CP1252, näevad UTF-C-s välja samasugused.

Siin tekib aga raskus: kuidas saada põhitähestikust abi? Võite jätta sama nihke, kuid - paraku - siin mängib Unicode'i struktuur juba meie vastu. Väga sageli ei asu tähestiku põhiosa ploki alguses (näiteks vene suurtähel “A” on kood 0x0410, kuigi kirillitsa plokk algab tähega 0x0400). Seega, kui oleme salvestanud esimesed 64 tähemärki, võime kaotada juurdepääsu tähestiku sabaosale.

Selle probleemi lahendamiseks käisin käsitsi läbi mõned erinevatele keeltele vastavad plokid ja määrasin nende jaoks põhitähestiku nihke. Ladina tähestik oli erandina üldiselt ümber järjestatud nagu base64.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8

Viimased lihvid

Mõelgem lõpuks sellele, kus saaks veel midagi paremaks muuta.

Pange tähele, et vorming 101xxxxx xxxxxxxx xxxxxxxx võimaldab kodeerida numbreid kuni 0x1FFFFF, ja Unicode lõpeb varem, kell 0x10FFFF. Teisisõnu, viimane koodipunkt esitatakse kujul 10110000 11111111 11111111. Seetõttu võime öelda, et kui esimene bait on kujul 1011xxxx (Kus xxxx suurem kui 0), tähendab see midagi muud. Näiteks saate sinna lisada veel 15 märki, mis on pidevalt saadaval ühes baidis kodeerimiseks, kuid ma otsustasin seda teha teisiti.

Vaatame nüüd neid Unicode'i plokke, mis nõuavad kolme baiti. Põhimõtteliselt, nagu juba mainitud, on need hiina tähemärgid - kuid nendega on raske midagi teha, neid on 21 tuhat. Aga sinna lendasid ka hiragana ja katakana - ega neid enam nii palju pole, alla kahesaja. Ja kuna me mäletasime jaapanlasi, on seal ka emotikone (tegelikult on need Unicode'is paljudes kohtades laiali, kuid peamised plokid on vahemikus 0x1F300 - 0x1FBFF). Kui mõelda sellele, et nüüd on emotikone, mis on kokku pandud mitmest koodipunktist korraga (näiteks emotikon ‍‍‍Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8 koosneb koguni 7 koodist!), siis muutub igale kolme baiti kulutamine täielikuks häbiks (7×3 = 21 baiti ühe ikooni nimel, õudusunenägu).

Seetõttu valime mõned valitud vahemikud, mis vastavad emotikonidele, hiraganale ja katakanale, nummerdame need ümber üheks pidevaks loendiks ja kodeerime need kahe baidina kolme asemel:

1011xxxx xxxxxxxx

Suurepärane: ülalmainitud emotikonVeel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8, mis koosneb 7 koodipunktist, võtab UTF-8-s 25 baiti ja me mahutame selle sisse 14 (täpselt kaks baiti iga koodipunkti kohta). Muide, Habr keeldus seda seedimast (nii vanas kui ka uues toimetajas), mistõttu pidin selle koos pildiga sisestama.

Proovime veel ühe probleemi lahendada. Nagu mäletame, on põhitähestik sisuliselt selline kõrge 6 bitti, mida peame meeles ja liimime iga järgmise dekodeeritud sümboli koodi külge. Plokis olevate hiina märkide puhul 0x4E00 - 0x9FFF, see on kas bitt 0 või 1. See pole eriti mugav: peame pidevalt nende kahe väärtuse vahel tähestikku vahetama (st kulutama kolm baiti). Kuid pange tähele, et pikas režiimis saame koodist endast lahutada märkide arvu, mille me lühikese režiimi abil kodeerime (pärast kõiki ülalkirjeldatud trikke on see 10240) - siis nihkub hieroglüüfide vahemik 0x2600 - 0x77FF, ja sel juhul on kogu selles vahemikus kõige olulisemad 6 bitti (21-st) võrdsed 0-ga. Seega kasutavad hieroglüüfide jadad kahte baiti hieroglüüfi kohta (mis on nii suure vahemiku jaoks optimaalne), ilma põhjustab tähestiku lülitusi.

Alternatiivsed lahendused: SCSU, BOCU-1

Unicode'i eksperdid, olles just artikli pealkirja lugenud, kiirustavad teile tõenäoliselt meelde tuletama, et Unicode'i standardite hulgas on Unicode'i standardne tihendusskeem (SCSU), mis kirjeldab artiklis kirjeldatule väga sarnast kodeerimismeetodit.

Tunnistan ausalt: sain selle olemasolust teada alles pärast seda, kui olin oma otsuse kirjutamisse sügavalt süvenenud. Kui ma oleksin sellest algusest peale teadnud, oleksin ilmselt proovinud kirjutada teostust, selle asemel, et oma lähenemisviisi välja mõelda.

Huvitav on see, et SCSU kasutab ideid, mis on väga sarnased nendega, mille ma ise välja mõtlesin (mõiste "tähestik" asemel kasutavad nad "aknaid" ja neid on saadaval rohkem kui mul). Samas on sellel vormingul ka miinuseid: see on pakkimisalgoritmidele veidi lähemal kui kodeerimisalgoritmidele. Eelkõige annab standard palju esitusmeetodeid, kuid ei ütle, kuidas optimaalset valida - selleks peab kodeerija kasutama mingit heuristikat. Seega on head pakendit tootev SCSU-kooder keerulisem ja kohmakam kui minu algoritm.

Võrdluseks tõin JavaScripti üle SCSU suhteliselt lihtsa teostuse - koodimahu poolest osutus see võrreldavaks minu UTF-C-ga, kuid mõnel juhul oli tulemus kümneid protsente kehvem (vahel võib seda ületada, aga mitte palju). Näiteks heebrea ja kreekakeelsed tekstid kodeeriti UTF-C-ga 60% parem kui SCSU (tõenäoliselt nende kompaktse tähestiku tõttu).

Eraldi lisan, et lisaks SCSU-le on veel üks viis Unicode'i kompaktseks esitamiseks - BOCU-1, kuid selle eesmärk on MIME-ühilduvus (mida ma ei vajanud) ja läheneb kodeerimisele veidi erinevalt. Ma ei ole selle tõhusust hinnanud, kuid mulle tundub, et see pole tõenäoliselt kõrgem kui SCSU.

Võimalikud parandused

Esitletud algoritm ei ole disainilt universaalne (see on ilmselt koht, kus minu eesmärgid erinevad Unicode'i konsortsiumi eesmärkidest kõige rohkem). Mainisin juba, et see töötati välja eelkõige ühe ülesande jaoks (mitmekeelse sõnastiku salvestamine eesliidete puusse) ja mõned selle funktsioonid ei pruugi teiste ülesannete jaoks hästi sobida. Kuid asjaolu, et see pole standard, võib olla pluss - saate seda lihtsalt oma vajaduste järgi muuta.

Näiteks saate ilmselgel viisil vabaneda oleku olemasolust, teha olekuta kodeerimist - lihtsalt ärge värskendage muutujaid offs, auxOffs и is21Bit kodeerijas ja dekoodris. Sel juhul ei ole võimalik sama tähestiku tähemärkide jadasid tõhusalt pakkida, kuid on garantii, et sama märk on alati kodeeritud samade baitidega, olenemata kontekstist.

Lisaks saate kohandada kodeerijat konkreetsele keelele, muutes vaikeolekut - näiteks keskendudes venekeelsetele tekstidele, seadistage alguses kodeerija ja dekooder offs = 0x0400 и auxOffs = 0. See on eriti mõttekas olekuta režiimi puhul. Üldiselt sarnaneb see vana kaheksa-bitise kodeeringu kasutamisega, kuid ilma et kaotataks võimalus sisestada tähemärke kogu Unicode'ist vastavalt vajadusele.

Teine varem mainitud puudus on see, et suures UTF-C-koodis tekstis ei ole kiiret viisi suvalisele baidile lähima märgipiiri leidmiseks. Kui lõikate kodeeritud puhvrist ära näiteks viimased 100 baiti, võite saada prügi, millega ei saa midagi peale hakata. Kodeering ei ole mõeldud mitme gigabaidiste logide salvestamiseks, kuid üldiselt saab seda parandada. Bait 0xBF ei tohi kunagi esineda esimese baidina (aga võib olla ka teine ​​või kolmas). Seetõttu saate kodeerimisel jada sisestada 0xBF 0xBF 0xBF iga, ütleme, 10 KB – siis, kui on vaja leida piir, piisab valitud tüki skannimisest, kuni leitakse sarnane marker. Viimase järgi 0xBF on garanteeritud tegelase algus. (Dekodeerimisel tuleb seda kolmebaidist jada loomulikult ignoreerida.)

Kokkuvõtteks

Kui olete siiani lugenud, palju õnne! Loodan, et nagu mina, õppisite midagi uut (või värskendasite oma mälu) Unicode'i struktuuri kohta.

Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8
Demo leht. Heebrea keele näide näitab eeliseid nii UTF-8 kui ka SCSU ees.

Ülalkirjeldatud uuringuid ei tohiks pidada standarditesse tungimiseks. Siiski olen üldiselt oma töö tulemustega rahul, seega olen nendega rahul jagada: näiteks minimeeritud JS-teek kaalub ainult 1710 baiti (ja sellel pole loomulikult mingeid sõltuvusi). Nagu ma eespool mainisin, võib tema tööd leida aadressil demo leht (seal on ka hulk tekste, mille põhjal saab seda võrrelda UTF-8 ja SCSU-ga).

Lõpuks juhin veel kord tähelepanu UTF-C kasutamise juhtudele mitte seda väärt:

  • Kui teie read on piisavalt pikad (100–200 tähemärki). Sel juhul peaksite mõtlema tihendusalgoritmide (nt tühjendamine) kasutamisele.
  • Kui vajate ASCII läbipaistvus, see tähendab, et teie jaoks on oluline, et kodeeritud jadad ei sisaldaks ASCII koode, mis ei olnud algses stringis. Selle vajadust saab vältida, kui edastate kolmanda osapoole API-dega suheldes (näiteks andmebaasiga töötades) kodeeringu tulemuse abstraktse baitide komplektina, mitte stringidena. Vastasel juhul võite saada ootamatuid haavatavusi.
  • Kui soovite kiiresti leida märgipiirid suvalise nihkega (näiteks kui osa reast on kahjustatud). Seda saab teha, kuid ainult skannides rida algusest peale (või rakendades eelmises jaotises kirjeldatud muudatust).
  • Kui teil on vaja kiiresti stringide sisuga toiminguid teha (sorteerida, otsida neist alamstringe, ühendada). Selleks tuleb kõigepealt dekodeerida stringid, nii et UTF-C on sellistel juhtudel aeglasem kui UTF-8 (kuid kiirem kui tihendusalgoritmid). Kuna sama stringi kodeeritakse alati ühtemoodi, ei ole dekodeerimise täpne võrdlus vajalik ja seda saab teha bait-baidipõhiselt.

Värskenda: kasutaja Tjomitš allolevates kommentaarides postitas graafiku, mis tõstab esile UTF-C rakendatavuse piirid. See näitab, et UTF-C on tõhusam kui üldotstarbeline tihendusalgoritm (LZW variatsioon), kui pakitud string on lühem ~140 tähemärki (Siiski märgin, et võrdlus viidi läbi ühe tekstiga; teiste keelte puhul võib tulemus erineda).
Veel üks jalgratas: säilitame Unicode'i stringe 30–60% kompaktsemalt kui UTF-8

Allikas: www.habr.com

Lisa kommentaar