Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8

Nëse jeni një zhvillues dhe jeni përballur me detyrën e zgjedhjes së një kodimi, atëherë Unicode do të jetë pothuajse gjithmonë zgjidhja e duhur. Metoda specifike e përfaqësimit varet nga konteksti, por më shpesh ekziston një përgjigje universale edhe këtu - UTF-8. Gjëja e mirë për këtë është se ju lejon të përdorni të gjithë karakteret e Unicode pa shpenzuar gjithashtu shumë bajt në shumicën e rasteve. Vërtetë, për gjuhët që përdorin më shumë sesa thjesht alfabetin latin, "jo shumë" është të paktën. dy bajt për karakter. A mund të bëjmë më mirë pa u kthyer te kodimet parahistorike që na kufizojnë në vetëm 256 karaktere të disponueshme?

Më poshtë unë propozoj të njiheni me përpjekjen time për t'iu përgjigjur kësaj pyetjeje dhe të zbatoni një algoritëm relativisht të thjeshtë që ju lejon të ruani linja në shumicën e gjuhëve të botës pa shtuar tepricën që është në UTF-8.

Mohim përgjegjësie. Do të bëj menjëherë disa rezerva të rëndësishme: zgjidhja e përshkruar nuk ofrohet si një zëvendësim universal për UTF-8, është i përshtatshëm vetëm në një listë të ngushtë rastesh (më shumë për to më poshtë), dhe në asnjë rast nuk duhet të përdoret për të bashkëvepruar me API-të e palëve të treta (të cilët as nuk dinë për të). Më shpesh, algoritmet e kompresimit me qëllime të përgjithshme (për shembull, deflate) janë të përshtatshme për ruajtjen kompakte të vëllimeve të mëdha të të dhënave tekstuale. Për më tepër, tashmë në procesin e krijimit të zgjidhjes sime, gjeta një standard ekzistues në vetë Unicode, i cili zgjidh të njëjtin problem - është disi më i ndërlikuar (dhe shpesh më i keq), por megjithatë është një standard i pranuar, dhe jo vetëm i vendosur së bashku në gju. Unë do t'ju tregoj edhe për të.

Rreth Unicode dhe UTF-8

Për të filluar, disa fjalë për atë që është Unicode и UTF-8.

Siç e dini, kodimet 8-bit dikur ishin të njohura. Me ta, gjithçka ishte e thjeshtë: 256 karaktere mund të numërohen me numra nga 0 në 255, dhe numrat nga 0 në 255 padyshim mund të përfaqësohen si një bajt. Nëse kthehemi në fillim, kodimi ASCII është plotësisht i kufizuar në 7 bit, kështu që biti më domethënës në paraqitjen e tij të bajtit është zero, dhe shumica e kodimeve 8-bitësh janë të pajtueshëm me të (ata ndryshojnë vetëm në "sipërme" pjesa, ku biti më domethënës është një).

Si ndryshon Unicode nga ato kodime dhe pse lidhen kaq shumë paraqitje specifike me të - UTF-8, UTF-16 (BE dhe LE), UTF-32? Le ta zgjidhim me radhë.

Standardi bazë i Unicode përshkruan vetëm korrespondencën midis karaktereve (dhe në disa raste, përbërësve individualë të karaktereve) dhe numrave të tyre. Dhe ka shumë numra të mundshëm në këtë standard - nga 0x00 tek 0x10FFFF (1 copë). Nëse do të donim të vendosnim një numër në një interval të tillë në një ndryshore, nuk do të na mjaftonin as 114 dhe as 112 bajt. Dhe meqenëse procesorët tanë nuk janë shumë të dizajnuar për të punuar me numra tre-bajtë, ne do të detyroheshim të përdorim deri në 1 bajt për karakter! Ky është UTF-2, por është pikërisht për shkak të kësaj "shpërdorimi" që ky format nuk është i popullarizuar.

Për fat të mirë, rendi i karaktereve brenda Unicode nuk është i rastësishëm. I gjithë grupi i tyre është i ndarë në 17"aeroplanët", secila prej të cilave përmban 65536 (0x10000) «pikat e kodit" Koncepti i një "pike kodi" këtu është thjesht numri i karakterit, i caktuar nga Unicode. Por, siç u përmend më lart, në Unicode numërohen jo vetëm karakteret individuale, por edhe përbërësit e tyre dhe shenjat e shërbimit (dhe ndonjëherë asgjë nuk korrespondon me numrin - ndoshta për momentin, por për ne kjo nuk është aq e rëndësishme), kështu që është më e saktë të flasim gjithmonë në mënyrë specifike për numrin e vetë numrave, dhe jo për simbolet. Megjithatë, në vijim, për hir të shkurtësisë, do të përdor shpesh fjalën "simbol", duke nënkuptuar termin "pika e kodit".

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Avionët Unicode. Siç mund ta shihni, shumica e tij (aeroplanët 4 deri në 13) është ende i papërdorur.

Ajo që është më e habitshme është se e gjithë "pulpa" kryesore qëndron në rrafshin zero, quhet "Plani bazë shumëgjuhësh". Nëse një rresht përmban tekst në një nga gjuhët moderne (përfshirë gjuhën kineze), ju nuk do të shkoni përtej këtij plani. Por nuk mund të shkëputni as pjesën tjetër të Unicode - për shembull, emoji ndodhen kryesisht në fund të avioni i radhës"Plani shumëgjuhësh plotësues"(shtrihet nga 0x10000 tek 0x1FFFF). Pra, UTF-16 e bën këtë: të gjithë personazhet bien brenda Plani bazë shumëgjuhësh, janë të koduara "siç është" me një numër përkatës prej dy bajtësh. Sidoqoftë, disa nga numrat në këtë varg nuk tregojnë fare karaktere specifike, por tregojnë se pas këtij çifti bajtësh duhet të marrim parasysh një tjetër - duke kombinuar vlerat e këtyre katër bajteve së bashku, marrim një numër që mbulon të gjithë gamën e vlefshme të Unicode. Kjo ide quhet "çifte surrogate"—mund të keni dëgjuar për to.

Pra, UTF-16 kërkon dy ose (në raste shumë të rralla) katër bajt për "pikë kodi". Kjo është më mirë sesa përdorimi i katër bajtëve gjatë gjithë kohës, por latinishtja (dhe karaktere të tjera ASCII) kur kodohen në këtë mënyrë humbin gjysmën e hapësirës në zero. UTF-8 është krijuar për të korrigjuar këtë: ASCII në të zë, si më parë, vetëm një bajt; kodet nga 0x80 tek 0x7FF - dy bajt; nga 0x800 tek 0xFFFF - tre, dhe nga 0x10000 tek 0x10FFFF - katër. Nga njëra anë, alfabeti latin është bërë i mirë: përputhshmëria me ASCII është rikthyer dhe shpërndarja është "shpërndarë" më e barabartë nga 1 në 4 bajt. Por alfabetet e tjera përveç latinishtes, mjerisht, nuk përfitojnë në asnjë mënyrë në krahasim me UTF-16, dhe shumë prej tyre tani kërkojnë tre bajt në vend të dy - diapazoni i mbuluar nga një rekord prej dy bajtësh është ngushtuar me 32 herë, me 0xFFFF tek 0x7FF, dhe as kineze dhe, për shembull, gjeorgjiane nuk përfshihen në të. cirilik dhe pesë alfabete të tjera - hurray - me fat, 2 bajt për karakter.

Pse ndodh kjo? Le të shohim se si UTF-8 përfaqëson kodet e karaktereve:
Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Direkt për të përfaqësuar numrat, këtu përdoren bitët e shënuar me simbolin x. Mund të shihet se në një rekord prej dy bajtësh ka vetëm 11 bit të tillë (nga 16). Bitet kryesore këtu kanë vetëm një funksion ndihmës. Në rastin e një regjistrimi me katër bajtë, 21 nga 32 bit ndahen për numrin e pikës së kodit - do të duket se tre bajt (që japin gjithsej 24 bit) do të mjaftonin, por shënuesit e shërbimit hanë shumë.

A është kjo e keqe? Jo ne te vertete. Nga njëra anë, nëse kujdesemi shumë për hapësirën, ne kemi algoritme kompresimi që mund të eliminojnë lehtësisht të gjithë entropinë dhe tepricën shtesë. Nga ana tjetër, qëllimi i Unicode ishte të siguronte kodimin më universal të mundshëm. Për shembull, ne mund t'i besojmë një linje të koduar në UTF-8 për të koduar që më parë ka punuar vetëm me ASCII dhe të mos kemi frikë se do të shohë një karakter nga diapazoni ASCII që në fakt nuk është aty (në fund të fundit, në UTF-8 të gjitha byte duke filluar me nga biti zero - kjo është pikërisht ajo që është ASCII). Dhe nëse papritmas duam të presim një bisht të vogël nga një varg i madh pa e deshifruar atë që në fillim (ose të rivendosim një pjesë të informacionit pas një seksioni të dëmtuar), është e lehtë për ne të gjejmë kompensimin ku fillon një personazh (mjafton për të kapërcyer bajt që kanë një parashtesë bit 10).

Pse atëherë të shpikni diçka të re?

Në të njëjtën kohë, herë pas here ka situata kur algoritmet e kompresimit si deflate nuk janë të zbatueshme, por ju dëshironi të arrini ruajtjen kompakte të vargjeve. Personalisht e kam hasur këtë problem kur kam menduar për ndërtimin pema e prefiksit të ngjeshur për një fjalor të madh që përfshin fjalë në gjuhë arbitrare. Nga njëra anë, çdo fjalë është shumë e shkurtër, kështu që ngjeshja e saj do të jetë e paefektshme. Nga ana tjetër, zbatimi i pemës që konsiderova ishte projektuar në mënyrë që çdo bajt i vargut të ruajtur të gjeneronte një kulm të veçantë të pemës, kështu që minimizimi i numrit të tyre ishte shumë i dobishëm. Në bibliotekën time Az.js (Si në pimorfia2, mbi të cilin bazohet) një problem i ngjashëm mund të zgjidhet thjesht - vargjet e paketuara në DAWG-fjalor, i ruajtur aty në CP1251 e vjetër e mirë. Por, siç është e lehtë për t'u kuptuar, kjo funksionon mirë vetëm për një alfabet të kufizuar - një rresht në gjuhën kineze nuk mund të shtohet në një fjalor të tillë.

Më vete, do të doja të shënoja një nuancë tjetër të pakëndshme që lind kur përdorni UTF-8 në një strukturë të tillë të dhënash. Fotografia e mësipërme tregon se kur një karakter shkruhet si dy bajt, bitet që lidhen me numrin e tij nuk vijnë me radhë, por ndahen nga një palë bit. 10 në mes: 110xxxxx 10xxxxxx. Për shkak të kësaj, kur 6 bitet e poshtme të bajtit të dytë derdhen në kodin e karakterit (d.m.th., ndodh një tranzicion 1011111110000000), pastaj ndryshon edhe bajt i parë. Rezulton se shkronja "p" shënohet me bajt 0xD0 0xBF, dhe "r" tjetër është tashmë 0xD1 0x80. Në një pemë parashtese, kjo çon në ndarjen e nyjes mëmë në dy - një për prefiksin 0xD0, dhe një tjetër për 0xD1 (megjithëse i gjithë alfabeti cirilik mund të kodohej vetëm nga bajt i dytë).

çfarë mora

Përballë këtij problemi, vendosa të praktikoj luajtjen e lojërave me bit, dhe në të njëjtën kohë të njihem pak më mirë me strukturën e Unicode në tërësi. Rezultati ishte formati i kodimit UTF-C ("C" për kompakt), i cili shpenzon jo më shumë se 3 bajt për pikë kodi, dhe shumë shpesh ju lejon të shpenzoni vetëm një bajt shtesë për të gjithë linjën e koduar. Kjo çon në faktin se në shumë alfabete jo-ASCII një kodim i tillë rezulton të jetë 30-60% më kompakt se UTF-8.

Unë kam paraqitur shembuj të zbatimit të algoritmeve të kodimit dhe dekodimit në formë Bibliotekat JavaScript dhe Go, mund t'i përdorni lirisht në kodin tuaj. Por gjithsesi do të theksoj se në njëfarë kuptimi ky format mbetet një "biçikletë" dhe nuk e rekomandoj përdorimin e tij pa e kuptuar pse ju duhet. Ky është akoma më shumë një eksperiment sesa një "përmirësim serioz i UTF-8". Sidoqoftë, kodi atje është shkruar mjeshtërisht, në mënyrë koncize, me një numër të madh komentesh dhe mbulimi testimi.

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Rezultatet e testit dhe krahasimi me UTF-8

Unë gjithashtu bëra faqe demo, ku mund të vlerësoni performancën e algoritmit dhe më pas do t'ju tregoj më shumë për parimet dhe procesin e zhvillimit të tij.

Eliminimi i pjesëve të tepërta

Unë e mora UTF-8 si bazë, natyrisht. Gjëja e parë dhe më e dukshme që mund të ndryshohet në të është zvogëlimi i numrit të biteve të shërbimit në çdo bajt. Për shembull, bajti i parë në UTF-8 gjithmonë fillon me njërën 0, ose me 11 - një parashtesë 10 Vetëm bajtët e mëposhtëm e kanë atë. Le të zëvendësojmë prefiksin 11 mbi 1, dhe për bajtet e radhës do t'i heqim plotësisht prefikset. Çfarë do të ndodhë?

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

Prisni, ku është rekordi me katër bajtë? Por nuk është më e nevojshme - kur shkruajmë në tre bajt, tani kemi 21 bit në dispozicion dhe kjo është e mjaftueshme për të gjithë numrat deri në 0x10FFFF.

Çfarë kemi sakrifikuar këtu? Gjëja më e rëndësishme është zbulimi i kufijve të karaktereve nga një vendndodhje arbitrare në tampon. Ne nuk mund të tregojmë në një bajt arbitrar dhe të gjejmë fillimin e karakterit tjetër prej tij. Ky është një kufizim i formatit tonë, por në praktikë kjo është rrallë e nevojshme. Zakonisht ne jemi në gjendje të kalojmë nëpër tampon që nga fillimi (veçanërisht kur bëhet fjalë për linja të shkurtra).

Situata me mbulimin e gjuhëve me 2 bajt është bërë gjithashtu më e mirë: tani formati me dy bajtë jep një gamë prej 14 bitësh, dhe këto janë kode deri në 0x3FFF. Kinezët janë të pafat (personazhet e tyre kryesisht variojnë nga 0x4E00 tek 0x9FFF), por gjeorgjianët dhe shumë popuj të tjerë argëtohen më shumë - gjuhët e tyre gjithashtu përshtaten në 2 bajt për karakter.

Futni gjendjen e koduesit

Le të mendojmë tani për vetitë e vetë linjave. Fjalori më së shpeshti përmban fjalë të shkruara me shkronja të të njëjtit alfabet dhe kjo vlen edhe për shumë tekste të tjera. Do të ishte mirë që ky alfabet të tregohet një herë, dhe më pas të tregohet vetëm numri i shkronjës brenda tij. Le të shohim nëse renditja e karaktereve në tabelën Unicode do të na ndihmojë.

Siç u përmend më lart, Unicode ndahet në aeroplan 65536 kode secila. Por kjo nuk është një ndarje shumë e dobishme (siç u tha tashmë, më shpesh jemi në rrafshin zero). Më interesante është ndarja sipas blloqe. Këto vargje nuk kanë më një gjatësi fikse dhe janë më domethënëse - si rregull, secili kombinon karaktere nga i njëjti alfabet.

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Një bllok që përmban karaktere të alfabetit Bengali. Fatkeqësisht, për arsye historike, ky është një shembull i paketimit jo shumë të dendur - 96 karaktere janë shpërndarë në mënyrë kaotike në 128 pika të kodit të bllokut.

Fillimet e blloqeve dhe madhësitë e tyre janë gjithmonë shumëfisha të 16 - kjo bëhet thjesht për lehtësi. Për më tepër, shumë blloqe fillojnë dhe mbarojnë me vlera që janë shumëfish të 128 ose edhe 256 - për shembull, alfabeti bazë cirilik merr 256 bajt nga 0x0400 tek 0x04FF. Kjo është mjaft e përshtatshme: nëse e ruajmë prefiksin një herë 0x04, atëherë çdo karakter cirilik mund të shkruhet në një bajt. Vërtetë, në këtë mënyrë do të humbasim mundësinë për t'u kthyer në ASCII (dhe në çdo personazh tjetër në përgjithësi). Prandaj ne bëjmë këtë:

  1. Dy bajt 10yyyyyy yxxxxxxx jo vetëm që shënojnë një simbol me një numër yyyyyy yxxxxxxx, por edhe ndryshim alfabeti aktual mbi yyyyyy y0000000 (d.m.th. ne i mbajmë mend të gjitha pjesët përveç atyre më pak të rëndësishme Pak 7);
  2. Një bajt 0xxxxxxx ky është karakteri i alfabetit aktual. Thjesht duhet t'i shtohet kompensimit që kujtuam në hapin 1. Ndërsa nuk e ndryshuam alfabetin, kompensimi është zero, kështu që ruajtëm përputhshmërinë me ASCII.

Po kështu për kodet që kërkojnë 3 bajt:

  1. Tre bajt 110yyyyy yxxxxxxx xxxxxxxx tregoni një simbol me një numër yyyyyy yxxxxxxx xxxxxxxx, ndryshim alfabeti aktual mbi yyyyyy y0000000 00000000 (Kujtova gjithçka përveç të rinjve Pak 15), dhe kontrolloni kutinë në të cilën jemi tani gjatë modaliteti (kur ndryshojmë alfabetin në një me dy bajt, ne do ta rivendosim këtë flamur);
  2. Dy bajt 0xxxxxxx xxxxxxxx në modalitetin e gjatë është karakteri i alfabetit aktual. Në mënyrë të ngjashme, ne e shtojmë atë me kompensimin nga hapi 1. I vetmi ndryshim është se tani lexojmë dy bajt (sepse kaluam në këtë mënyrë).

Tingëllon mirë: tani, ndërsa na duhet të kodojmë karaktere nga i njëjti gamë Unicode 7-bit, ne shpenzojmë 1 bajt shtesë në fillim dhe gjithsej një bajt për karakter.

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Duke punuar nga një nga versionet e mëparshme. Ajo tashmë shpesh mund UTF-8, por ka ende vend për përmirësim.

Çfarë ka më keq? Së pari, ne kemi një kusht, domethënë kompensimi aktual i alfabetit dhe kutinë e zgjedhjes modaliteti i gjatë. Kjo na kufizon më tej: tani të njëjtat karaktere mund të kodohen ndryshe në kontekste të ndryshme. Kërkimi i nënvargjeve, për shembull, do të duhet të bëhet duke marrë parasysh këtë, dhe jo vetëm duke krahasuar bajtet. Së dyti, sapo ndryshuam alfabetin, u bë keq me kodimin e karaktereve ASCII (dhe ky nuk është vetëm alfabeti latin, por edhe shenjat e pikësimit, përfshirë hapësirat) - ata kërkojnë ndryshimin e alfabetit përsëri në 0, domethënë, përsëri një bajt shtesë (dhe më pas një tjetër për t'u kthyer në pikën tonë kryesore).

Një alfabet është i mirë, dy është më i mirë

Le të përpiqemi të ndryshojmë pak parashtesat tona të biteve, duke shtrydhur një më shumë në tre të përshkruara më sipër:

0xxxxxxx — 1 bajt në modalitetin normal, 2 në modalitetin e gjatë
11xxxxxx - 1 bajt
100xxxxx xxxxxxxx - 2 bajt
101xxxxx xxxxxxxx xxxxxxxx - 3 bajt

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8

Tani në një rekord prej dy bajtësh ka një bit më pak të disponueshëm - kodi tregon deri në 0x1FFFDhe jo 0x3FFF. Sidoqoftë, është ende dukshëm më i madh se në kodet UTF-8 me dy bajt, gjuhët më të zakonshme ende përshtaten, humbja më e dukshme ka rënë hiragana и katakanaJaponezët janë të trishtuar.

Cili është ky kod i ri? 11xxxxxx? Ky është një "stash" i vogël prej 64 karakteresh në madhësi, ai plotëson alfabetin tonë kryesor, kështu që unë e quajta atë ndihmës (ndihmëse) alfabeti. Kur ndërrojmë alfabetin aktual, një pjesë e alfabetit të vjetër bëhet ndihmëse. Për shembull, ne kaluam nga ASCII në cirilik - ruajtja tani përmban 64 karaktere që përmbajnë Alfabeti latin, numrat, hapësira dhe presja (insertimet më të shpeshta në tekste jo-ASCII). Kthehuni në ASCII - dhe pjesa kryesore e alfabetit cirilik do të bëhet alfabeti ndihmës.

Falë aksesit në dy alfabete, ne mund të trajtojmë një numër të madh tekstesh me kosto minimale për ndërrimin e alfabeteve (shenjat e pikësimit më së shpeshti do të çojnë në kthimin në ASCII, por pas kësaj do të marrim shumë karaktere jo-ASCII nga alfabeti shtesë, pa ndërrimi përsëri).

Bonus: parashtesa e nënalfabetit 11xxxxxx dhe duke zgjedhur kompensimin e tij fillestar që të jetë 0xC0, marrim përputhshmëri të pjesshme me CP1252. Me fjalë të tjera, shumë (por jo të gjitha) tekste të Evropës Perëndimore të koduara në CP1252 do të duken njësoj në UTF-C.

Këtu, megjithatë, lind një vështirësi: si të merrni një ndihmës nga alfabeti kryesor? Mund të lini të njëjtin kompensim, por - mjerisht - këtu struktura Unicode tashmë po luan kundër nesh. Shumë shpesh pjesa kryesore e alfabetit nuk është në fillim të bllokut (për shembull, kryeqyteti rus "A" ka kodin 0x0410, megjithëse blloku cirilik fillon me 0x0400). Kështu, duke marrë 64 karakteret e para në ruajtje, ne mund të humbasim aksesin në bishtin e alfabetit.

Për të zgjidhur këtë problem, unë kalova manualisht nëpër disa blloqe që korrespondojnë me gjuhë të ndryshme dhe specifikova zhvendosjen e alfabetit ndihmës brenda atij kryesor për to. Alfabeti latin, si përjashtim, në përgjithësi u rirendit si bazë64.

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8

Prekjet përfundimtare

Le të mendojmë më në fund se ku mund të përmirësojmë diçka tjetër.

Vini re se formati 101xxxxx xxxxxxxx xxxxxxxx ju lejon të kodoni numrat deri në 0x1FFFFF, dhe Unicode përfundon më herët, në 0x10FFFF. Me fjalë të tjera, pika e fundit e kodit do të përfaqësohet si 10110000 11111111 11111111. Prandaj, mund të themi se nëse bajt i parë është i formës 1011xxxx (ku xxxx më i madh se 0), atëherë do të thotë diçka tjetër. Për shembull, mund të shtoni 15 karaktere të tjera atje që janë vazhdimisht të disponueshme për kodim në një bajt, por vendosa ta bëj ndryshe.

Le të shohim ato blloqe Unicode që kërkojnë tre bajt tani. Në thelb, siç u përmend tashmë, këto janë karaktere kineze - por është e vështirë të bësh ndonjë gjë me ta, ka 21 mijë prej tyre. Por hiragana dhe katakana gjithashtu fluturuan atje - dhe nuk ka më aq shumë prej tyre, më pak se dyqind. Dhe, meqenëse kujtuam japonezët, ka edhe emoji (në fakt, ato janë të shpërndara në shumë vende në Unicode, por blloqet kryesore janë në gamë 0x1F300 - 0x1FBFF). Nëse mendoni për faktin se tani ka emoji që janë mbledhur nga disa pika kodi në të njëjtën kohë (për shembull, emojiNjë biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8 përbëhet nga 7 kode!), atëherë bëhet turp i plotë të shpenzosh tre bajt për secilin (7×3 = 21 bajt për hir të një ikone, një makth).

Prandaj, ne zgjedhim disa vargje të zgjedhura që korrespondojnë me emoji, hiragana dhe katakana, i rinumërojmë në një listë të vazhdueshme dhe i kodojmë si dy bajt në vend të tre:

1011xxxx xxxxxxxx

E shkëlqyeshme: emoji i lartpërmendurNjë biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8, i përbërë nga 7 pika kodi, merr 8 bajt në UTF-25 dhe ne e vendosim atë në 14 (saktësisht dy bajt për çdo pikë kodi). Meqë ra fjala, Habr nuk pranoi ta trette (si në redaktorin e vjetër ashtu edhe në atë të ri), ndaj më duhej ta fusja me një foto.

Le të përpiqemi të rregullojmë një problem tjetër. Siç e kujtojmë, alfabeti bazë është në thelb i lartë 6 bit, të cilin e mbajmë parasysh dhe e ngjisim kodin e çdo simboli të deshifruar të radhës. Në rastin e karaktereve kineze që janë në bllok 0x4E00 - 0x9FFF, ky është ose biti 0 ose 1. Kjo nuk është shumë e përshtatshme: do të na duhet të ndërrojmë vazhdimisht alfabetin midis këtyre dy vlerave (d.m.th. të shpenzojmë tre bajt). Por vini re se në modalitetin e gjatë, nga vetë kodi mund të zbresim numrin e karaktereve që kodojmë duke përdorur modalitetin e shkurtër (pas të gjitha mashtrimeve të përshkruara më lart, kjo është 10240) - atëherë diapazoni i hieroglifeve do të zhvendoset në 0x2600 - 0x77FF, dhe në këtë rast, gjatë gjithë këtij diapazoni, 6 bitet më domethënëse (nga 21) do të jenë të barabarta me 0. Kështu, sekuencat e hieroglifeve do të përdorin dy bajt për hieroglif (që është optimale për një gamë kaq të madhe), pa duke shkaktuar ndërrime të alfabetit.

Zgjidhje alternative: SCSU, BOCU-1

Ekspertët e Unicode, pasi sapo kanë lexuar titullin e artikullit, ka shumë të ngjarë të nxitojnë t'ju kujtojnë se direkt midis standardeve të Unicode ekziston Skema standarde e kompresimit për Unicode (SCSU), i cili përshkruan një metodë kodimi shumë të ngjashme me atë të përshkruar në artikull.

E pranoj sinqerisht: Mësova për ekzistencën e saj vetëm pasi u zhyta thellë në shkrimin e vendimit tim. Sikur ta kisha ditur që në fillim, me siguri do të isha përpjekur të shkruaja një zbatim në vend që të krijoja qasjen time.

Ajo që është interesante është se SCSU përdor ide shumë të ngjashme me ato që kam ardhur vetë (në vend të konceptit të "alfabeteve" ata përdorin "windows" dhe ka më shumë prej tyre në dispozicion se unë). Në të njëjtën kohë, ky format ka edhe disavantazhe: është pak më afër algoritmeve të kompresimit sesa ato të kodimit. Në veçanti, standardi jep shumë metoda përfaqësimi, por nuk thotë se si të zgjidhni atë optimale - për këtë, koduesi duhet të përdorë një lloj heuristike. Kështu, një kodues SCSU që prodhon paketim të mirë do të jetë më kompleks dhe më i rëndë se algoritmi im.

Për krahasim, unë transferova një zbatim relativisht të thjeshtë të SCSU në JavaScript - për sa i përket vëllimit të kodit doli të ishte i krahasueshëm me UTF-C tim, por në disa raste rezultati ishte dhjetëra përqind më i keq (ndonjëherë mund ta tejkalojë atë, por jo shumë). Për shembull, tekstet në hebraisht dhe greqisht ishin të koduara nga UTF-C 60% më mirë se SCSU (ndoshta për shkak të alfabeteve të tyre kompakte).

Më vete, do të shtoj se përveç SCSU ekziston edhe një mënyrë tjetër për të përfaqësuar në mënyrë kompakte Unicode - BOCU-1, por synon përputhshmërinë MIME (që nuk më duhej) dhe merr një qasje paksa të ndryshme për kodimin. Unë nuk e kam vlerësuar efektivitetin e tij, por më duket se nuk ka gjasa të jetë më i lartë se SCSU.

Përmirësime të mundshme

Algoritmi që prezantova nuk është universal nga dizajni (ky është ndoshta vendi ku qëllimet e mia ndryshojnë më shumë nga qëllimet e Konsorciumit Unicode). Unë kam përmendur tashmë se ai u zhvillua kryesisht për një detyrë (ruajtja e një fjalori shumëgjuhësh në një pemë parashtese) dhe disa nga veçoritë e tij mund të mos jenë të përshtatshme për detyra të tjera. Por fakti që nuk është një standard mund të jetë një plus - ju mund ta modifikoni lehtësisht për t'iu përshtatur nevojave tuaja.

Për shembull, në një mënyrë të qartë mund të heqësh qafe praninë e gjendjes, të bësh kodim pa shtetësi - thjesht mos përditëso variablat offs, auxOffs и is21Bit në kodues dhe dekoder. Në këtë rast, nuk do të jetë e mundur të paketohen në mënyrë efektive sekuencat e karaktereve të të njëjtit alfabet, por do të ketë një garanci që i njëjti karakter është gjithmonë i koduar me të njëjtat bajt, pavarësisht nga konteksti.

Përveç kësaj, ju mund ta përshtatni koduesin në një gjuhë specifike duke ndryshuar gjendjen e paracaktuar - për shembull, duke u fokusuar në tekstet ruse, vendosni koduesin dhe dekoderin në fillim offs = 0x0400 и auxOffs = 0. Kjo veçanërisht ka kuptim në rastin e regjimit pa shtetësi. Në përgjithësi, kjo do të jetë e ngjashme me përdorimin e kodimit të vjetër me tetë bit, por pa hequr aftësinë për të futur karaktere nga të gjithë Unicode sipas nevojës.

Një tjetër pengesë e përmendur më parë është se në tekstin e madh të koduar në UTF-C nuk ka asnjë mënyrë të shpejtë për të gjetur kufirin e karakterit më afër një bajt arbitrar. Nëse i shkëputni, të themi, 100 bajtët e fundit nga buferi i koduar, rrezikoni të merrni mbeturina me të cilat nuk mund të bëni asgjë. Kodimi nuk është krijuar për ruajtjen e regjistrave me shumë gigabajt, por në përgjithësi kjo mund të korrigjohet. Bajt 0xBF nuk duhet të shfaqet kurrë si bajt i parë (por mund të jetë i dyti ose i tretë). Prandaj, kur kodoni, mund të futni sekuencën 0xBF 0xBF 0xBF çdo, të themi, 10 KB - atëherë, nëse keni nevojë të gjeni një kufi, do të jetë e mjaftueshme të skanoni pjesën e zgjedhur derisa të gjendet një shënues i ngjashëm. Në vijim të fundit 0xBF garantohet të jetë fillimi i një personazhi. (Kur deshifrohet, kjo sekuencë prej tre bajtësh, natyrisht, do të duhet të shpërfillet.)

Duke përmbledhur

Nëse keni lexuar deri këtu, urime! Shpresoj që ju, si unë, të mësoni diçka të re (ose të keni rifreskuar kujtesën) në lidhje me strukturën e Unicode.

Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8
Faqe demo. Shembulli i hebraishtes tregon avantazhet ndaj UTF-8 dhe SCSU.

Hulumtimi i përshkruar më sipër nuk duhet të konsiderohet si shkelje e standardeve. Megjithatë, në përgjithësi jam i kënaqur me rezultatet e punës sime, ndaj jam i kënaqur me to pjesë: për shembull, një bibliotekë e minuar JS peshon vetëm 1710 bajt (dhe sigurisht nuk ka varësi). Siç e përmenda më lart, puna e saj mund të gjendet në faqe demo (ekziston gjithashtu një grup tekstesh mbi të cilat mund të krahasohet me UTF-8 dhe SCSU).

Së fundi, unë do të tërheq edhe një herë vëmendjen për rastet në të cilat përdoret UTF-C nuk ia vlen:

  • Nëse linjat tuaja janë mjaft të gjata (nga 100-200 karaktere). Në këtë rast, duhet të mendoni për përdorimin e algoritmeve të kompresimit si deflate.
  • Nëse keni nevojë transparenca ASCII, domethënë, është e rëndësishme për ju që sekuencat e koduara të mos përmbajnë kode ASCII që nuk ishin në vargun origjinal. Nevoja për këtë mund të shmanget nëse, kur ndërveproni me API-të e palëve të treta (për shembull, duke punuar me një bazë të dhënash), ju kaloni rezultatin e kodimit si një grup abstrakt bajtash dhe jo si vargje. Përndryshe, rrezikoni të merrni dobësi të papritura.
  • Nëse dëshironi të jeni në gjendje të gjeni shpejt kufijtë e karaktereve në një zhvendosje arbitrare (për shembull, kur një pjesë e një rreshti është dëmtuar). Kjo mund të bëhet, por vetëm duke skanuar linjën nga fillimi (ose duke aplikuar modifikimin e përshkruar në seksionin e mëparshëm).
  • Nëse keni nevojë të kryeni shpejt operacione në përmbajtjen e vargjeve (renditini ato, kërkoni për nënvargje në to, bashkojini). Kjo kërkon që vargjet të deshifrohen fillimisht, kështu që UTF-C do të jetë më i ngadalshëm se UTF-8 në këto raste (por më i shpejtë se algoritmet e kompresimit). Meqenëse i njëjti varg kodohet gjithmonë në të njëjtën mënyrë, krahasimi i saktë i dekodimit nuk kërkohet dhe mund të bëhet në bazë bajt pas bajt.

Update: përdoruesit Tyomitch në komentet më poshtë postoi një grafik duke theksuar kufijtë e zbatueshmërisë së UTF-C. Tregon se UTF-C është më efikas se një algoritëm kompresimi me qëllim të përgjithshëm (një variacion i LZW) për sa kohë që vargu i paketuar është më i shkurtër ~ 140 karaktere (megjithatë, vërej se krahasimi u krye në një tekst; për gjuhët e tjera rezultati mund të ndryshojë).
Një biçikletë tjetër: ne ruajmë vargjet Unicode 30-60% më kompakte se UTF-8

Burimi: www.habr.com

Shto një koment