Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8

Se vi estas programisto kaj vi alfrontas la taskon elekti kodigon, tiam Unikodo preskaŭ ĉiam estos la ĝusta solvo. La specifa reprezenta metodo dependas de la kunteksto, sed plej ofte ankaŭ ĉi tie estas universala respondo - UTF-8. La bona afero pri ĝi estas, ke ĝi permesas uzi ĉiujn Unikodajn signojn sen elspezo ankaŭ multaj bajtoj en la plej multaj kazoj. Vere, por lingvoj, kiuj uzas pli ol nur la latinan alfabeton, "ne tro multe" estas almenaŭ du bajtoj por signo. Ĉu ni povas fari pli bone sen reveni al prahistoriaj kodigoj, kiuj limigas nin al nur 256 disponeblaj signoj?

Malsupre mi proponas konatiĝi kun mia provo respondi ĉi tiun demandon kaj efektivigi relative simplan algoritmon, kiu permesas vin stoki liniojn en la plej multaj lingvoj de la mondo sen aldoni la redundon kiu estas en UTF-8.

Malgarantio. Mi tuj faros kelkajn gravajn rezervojn: la priskribita solvo ne estas ofertita kiel universala anstataŭaĵo por UTF-8, ĝi taŭgas nur en mallarĝa listo de kazoj (pli pri ili sube), kaj en neniu kazo ĝi estu uzata por interagi kun triaj API-oj (kiuj eĉ ne scias pri ĝi). Plej ofte, ĝeneraluzeblaj kunpremaj algoritmoj (ekzemple, malŝveligi) taŭgas por kompakta stokado de grandaj volumoj de tekstaj datumoj. Krome, jam en la procezo de kreado de mia solvo, mi trovis ekzistantan normon en Unikodo mem, kiu solvas la saman problemon - ĝi estas iom pli komplika (kaj ofte pli malbona), sed tamen ĝi estas akceptata normo, kaj ne nur metata. kune sur la genuo. Mi rakontos al vi ankaŭ pri li.

Pri Unikodo kaj UTF-8

Komence, kelkajn vortojn pri kio ĝi estas Unikodo и UTF-8.

Kiel vi scias, 8-bitaj kodigoj antaŭe estis popularaj. Ĉe ili ĉio estis simpla: 256 signoj povas esti numeritaj per ciferoj de 0 ĝis 255, kaj nombroj de 0 ĝis 255 evidente prezenteblas kiel unu bajto. Se ni reiras al la komenco mem, la ASCII-kodigo estas tute limigita al 7 bitoj, do la plej signifa bito en sia bajta reprezentado estas nulo, kaj la plej multaj 8-bita kodigado estas kongrua kun ĝi (ili diferencas nur en la "supera" parto, kie la plej signifa bito estas unu ).

Kiel Unikodo diferencas de tiuj kodigoj kaj kial tiom da specifaj prezentoj rilatas al ĝi - UTF-8, UTF-16 (BE kaj LE), UTF-32? Ni ordigu ĝin en ordo.

La baza Unikoda normo priskribas nur la korespondadon inter signoj (kaj en kelkaj kazoj, individuaj komponentoj de signoj) kaj iliaj nombroj. Kaj estas multaj eblaj nombroj en ĉi tiu normo - de 0x00 por 0x10FFFF (1 pecoj). Se ni volus meti nombron en tia gamo en variablon, nek 114 nek 112 bajtoj sufiĉus por ni. Kaj ĉar niaj procesoroj ne estas tre dezajnitaj por labori kun tri-bajtaj nombroj, ni estus devigitaj uzi eĉ 1 bajtojn por signo! Ĉi tio estas UTF-2, sed ĝuste pro ĉi tiu "malŝparemo" ĉi tiu formato ne estas populara.

Feliĉe, la ordo de signoj ene de Unikodo ne estas hazarda. Ilia tuta aro estas dividita en 17"aviadiloj", ĉiu el kiuj enhavas 65536 (0x10000) "kodpunktoj" La koncepto de "kodpunkto" ĉi tie estas simple signo nombro, asignita al ĝi fare de Unikodo. Sed, kiel menciite supre, en Unikodo estas numeritaj ne nur unuopaj signoj, sed ankaŭ iliaj komponantoj kaj servomarkoj (kaj foje nenio respondas al la nombro - eble provizore, sed por ni tio ne tiom gravas), do pli ĝuste estas ĉiam paroli specife pri la nombro da nombroj mem, kaj ne simboloj. Tamen, en la sekvanta, por koncizeco, mi ofte uzos la vorton "simbolo", implicante la terminon "kodpunkto".

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Unikodaj aviadiloj. Kiel vi povas vidi, la plej granda parto de ĝi (aviadiloj 4 ĝis 13) estas ankoraŭ neuzata.

Plej rimarkinda estas, ke la tuta ĉefa "pulpo" kuŝas en la nula ebeno, ĝi nomiĝas "Baza Plurlingva Ebeno". Se linio enhavas tekston en unu el la modernaj lingvoj (inkluzive de la ĉina), vi ne iros preter ĉi tiu ebeno. Sed vi ankaŭ ne povas tranĉi la reston de Unikodo - ekzemple, emoji troviĝas ĉefe ĉe la fino de la sekva aviadilo,"Suplementa Plurlingva Ebeno"(ĝi etendiĝas de 0x10000 por 0x1FFFF). Do UTF-16 faras ĉi tion: ĉiuj signoj enfalantaj Baza Plurlingva Ebeno, estas koditaj "kiel estas" kun responda dubajta nombro. Tamen iuj el la nombroj en ĉi tiu gamo tute ne indikas specifajn signojn, sed indikas, ke post ĉi tiu paro da bajtoj ni devas konsideri alian - kombinante la valorojn de ĉi tiuj kvar bajtoj kune, ni ricevas nombron, kiu kovras. la tuta valida Unikoda gamo. Ĉi tiu ideo nomiĝas "surogataj paroj"—vi eble aŭdis pri ili.

Do UTF-16 postulas du aŭ (en tre maloftaj kazoj) kvar bajtojn per "kodpunkto". Ĉi tio estas pli bona ol uzi kvar bajtojn la tutan tempon, sed la latina (kaj aliaj ASCII-signoj) kiam kodita tiel malŝparas duonon de la spaco sur nuloj. UTF-8 estas desegnita por korekti ĉi tion: ASCII en ĝi okupas, kiel antaŭe, nur unu bajton; kodoj de 0x80 por 0x7FF - du bajtoj; de 0x800 por 0xFFFF - tri, kaj de 0x10000 por 0x10FFFF - kvar. Unuflanke, la latina alfabeto fariĝis bona: kongruo kun ASCII revenis, kaj la distribuo estas pli egale "disvastigita" de 1 ĝis 4 bajtoj. Sed alfabetoj krom la latina, ve, neniel profitas kompare kun UTF-16, kaj multaj nun postulas tri bajtojn anstataŭ du - la intervalo kovrita de dubajta rekordo mallarĝiĝis je 32 fojojn, kun 0xFFFF por 0x7FF, kaj nek la ĉina nek, ekzemple, la kartvela estas inkluzivita en ĝi. Cirila kaj kvin aliaj alfabetoj - hura - bonŝanca, 2 bajtoj por signo.

Kial ĉi tio okazas? Ni vidu kiel UTF-8 reprezentas signajn kodojn:
Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Rekte por reprezenti nombrojn, bitoj markitaj per la simbolo estas uzataj ĉi tie x. Videblas, ke en dubajta registro estas nur 11 tiaj bitoj (el 16). La gvidaj bitoj ĉi tie havas nur helpan funkcion. En la kazo de kvar-bajta rekordo, 21 el 32 bitoj estas asignitaj por la kodpunkto-nombro - ŝajnus, ke tri bajtoj (kiuj donas entute 24 bitoj) sufiĉus, sed servomarkoj tro manĝas.

Ĉu ĉi tio estas malbona? Ne vere. Unuflanke, se ni multe zorgas pri spaco, ni havas kunpremajn algoritmojn, kiuj povas facile forigi la tutan ekstran entropion kaj redundon. Aliflanke, la celo de Unikodo estis disponigi la plej universalan kodigon ebla. Ekzemple, ni povas konfidi linion kodita en UTF-8 al kodo, kiu antaŭe funkciis nur kun ASCII, kaj ne timi, ke ĝi vidos signon el la ASCII-gamo, kiu fakte ne estas tie (finfine, en UTF-8 ĉiuj bajtoj komencante per de la nula bito - ĝuste tio estas ASCII). Kaj se ni subite volas fortranĉi malgrandan voston de granda ŝnuro sen malkodi ĝin ekde la komenco (aŭ restarigi parton de la informo post difektita sekcio), estas facile por ni trovi la ofseton kie komenciĝas signo (sufiĉas). por salti bajtojn, kiuj havas iom-prefikson 10).

Kial do elpensi ion novan?

Samtempe, estas foje situacioj kiam kunpremaj algoritmoj kiel deflate estas malbone aplikeblaj, sed vi volas atingi kompaktan stokadon de ŝnuroj. Persone, mi renkontis ĉi tiun problemon pensante pri konstruado kunpremita prefiksa arbo por granda vortaro inkluzivanta vortojn en arbitraj lingvoj. Unuflanke, ĉiu vorto estas tre mallonga, do kunpremi ĝin estos senefika. Aliflanke, la arb-efektivigo kiun mi konsideris estis desegnita tiel ke ĉiu bajto de la stokita ĉeno generis apartan arbvertico, do minimumigi ilian nombron estis tre utila. En mia biblioteko Az.js (Kiel en pimorfio2, sur kiu ĝi baziĝas) simila problemo povas esti solvita simple - ŝnuroj pakitaj en DAWG-dictionary, stokita tie en bona malnova CP1251. Sed, kiel facile kompreneblas, tio funkcias bone nur por limigita alfabeto - linio en la ĉina ne povas esti aldonita al tia vortaro.

Aparte, mi ŝatus noti unu plian malagrablan nuancon, kiu aperas kiam vi uzas UTF-8 en tia datumstrukturo. La supra bildo montras, ke kiam signo estas skribita kiel du bajtoj, la bitoj rilataj al ĝia nombro ne venas en vico, sed estas apartigitaj per paro da bitoj. 10 meze: 110xxxxx 10xxxxxx. Pro tio, kiam la pli malaltaj 6 bitoj de la dua bajto superfluas en la signokodo (t.e., transiro okazas 1011111110000000), tiam ankaŭ la unua bajto ŝanĝiĝas. Rezultas, ke la litero "p" estas indikita per bajtoj 0xD0 0xBF, kaj la sekva "r" jam estas 0xD1 0x80. En prefiksa arbo, tio kondukas al disigo de la gepatra nodo en du - unu por la prefikso. 0xD0, kaj alia por 0xD1 (kvankam la tuta cirila alfabeto povus esti kodita nur per la dua bajto).

Kion mi ricevis

Fronte al ĉi tiu problemo, mi decidis praktiki ludojn per bitoj, kaj samtempe iom pli bone konatiĝi kun la strukturo de Unikodo entute. La rezulto estis la UTF-C-kodformato ("C" por kompakta), kiu elspezas ne pli ol 3 bajtojn per kodpunkto, kaj tre ofte permesas vin elspezi nur unu kroma bajto por la tuta kodita linio. Ĉi tio kondukas al la fakto, ke ĉe multaj ne-ASCII-alfabetoj tia kodado montriĝas 30-60% pli kompakta ol UTF-8.

Mi prezentis ekzemplojn de efektivigo de kodigaj kaj malkodaj algoritmoj en la formo Bibliotekoj JavaScript kaj Go, vi povas libere uzi ilin en via kodo. Sed mi ankoraŭ emfazos, ke iusence ĉi tiu formato restas "biciklo", kaj mi ne rekomendas uzi ĝin. sen kompreni kial vi bezonas ĝin. Ĉi tio ankoraŭ estas pli eksperimento ol serioza "plibonigo de UTF-8". Tamen, la kodo tie estas skribita bonorde, koncize, kun granda nombro da komentoj kaj testa kovrado.

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Testrezultoj kaj komparo kun UTF-8

Mi ankaŭ faris demo paĝo, kie vi povas taksi la agadon de la algoritmo, kaj tiam mi rakontos al vi pli pri ĝiaj principoj kaj evoluprocezo.

Forigante redundajn bitojn

Mi prenis UTF-8 kiel bazon, kompreneble. La unua kaj plej evidenta afero, kiu povas esti ŝanĝita en ĝi, estas redukti la nombron da servobitoj en ĉiu bajto. Ekzemple, la unua bajto en UTF-8 ĉiam komenciĝas per ambaŭ 0, aŭ kun 11 - prefikso 10 Nur la sekvaj bajtoj havas ĝin. Ni anstataŭigu la prefikson 11 sur 1, kaj por la sekvaj bajtoj ni tute forigos la prefiksojn. Kio okazos?

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

Atendu, kie estas la kvar-bajta rekordo? Sed ĝi ne plu necesas - skribante per tri bajtoj, ni nun havas disponeblajn 21 bitojn kaj tio sufiĉas por ĉiuj nombroj ĝis 0x10FFFF.

Kion ni oferis ĉi tie? La plej grava afero estas la detekto de signolimoj de arbitra loko en la bufro. Ni ne povas montri al arbitra bajto kaj trovi la komencon de la sekva signo el ĝi. Ĉi tio estas limigo de nia formato, sed praktike tio malofte estas necesa. Ni kutime kapablas trairi la bufron de la komenco (precipe kiam temas pri mallongaj linioj).

La situacio kun kovrado de lingvoj per 2 bajtoj ankaŭ pliboniĝis: nun la du-bajta formato donas gamon de 14 bitoj, kaj ĉi tiuj estas kodoj ĝis 0x3FFF. La ĉinoj estas malbonŝancaj (iliaj karakteroj plejparte intervalas de 0x4E00 por 0x9FFF), sed kartveloj kaj multaj aliaj popoloj pli amuzas - ankaŭ iliaj lingvoj konvenas en 2 bajtojn per signo.

Enigu la staton de kodilo

Ni nun pensu pri la propraĵoj de la linioj mem. La vortaro plej ofte enhavas vortojn skribitajn per signoj de la sama alfabeto, kaj tio validas ankaŭ por multaj aliaj tekstoj. Estus bone indiki ĉi tiun alfabeton unufoje, kaj poste indiki nur la nombron de la litero ene de ĝi. Ni vidu ĉu la aranĝo de signoj en la Unikoda tabelo helpos nin.

Kiel menciite supre, Unikodo estas dividita en aviadilo 65536 kodoj ĉiu. Sed ĉi tio ne estas tre utila divido (kiel jam dirite, plej ofte ni estas en la nula ebeno). Pli interesa estas la divido per blokoj. Ĉi tiuj gamoj ne plu havas fiksan longon, kaj estas pli signifaj - kiel regulo, ĉiu kombinas signojn de la sama alfabeto.

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Bloko enhavanta signojn de la bengala alfabeto. Bedaŭrinde, pro historiaj kialoj, ĉi tio estas ekzemplo de ne tre densa pakado - 96 signoj estas kaose disigitaj tra 128 blokkodpunktoj.

La komencoj de blokoj kaj iliaj grandecoj ĉiam estas multobloj de 16 - tio estas farita simple por oportuno. Krome, multaj blokoj komenciĝas kaj finiĝas sur valoroj, kiuj estas multobloj de 128 aŭ eĉ 256 - ekzemple, la baza cirila alfabeto okupas 256 bajtojn de 0x0400 por 0x04FF. Ĉi tio estas sufiĉe oportuna: se ni konservas la prefikson unufoje 0x04, tiam ajna cirila signo povas esti skribita en unu bajto. Vere, tiamaniere ni perdos la ŝancon reveni al ASCII (kaj al iuj aliaj karakteroj ĝenerale). Tial ni faras ĉi tion:

  1. Du bajtoj 10yyyyyy yxxxxxxx ne nur signi simbolon kun nombro yyyyyy yxxxxxxx, sed ankaŭ ŝanĝi aktuala alfabeto sur yyyyyy y0000000 (t.e. ni memoras ĉiujn pecojn krom la malplej signifaj 7a bito);
  2. Unu bajto 0xxxxxxx jen la signo de la nuna alfabeto. Ĝi nur devas esti aldonita al la ofseto, kiun ni memoris en la paŝo 1. Dum ni ne ŝanĝis la alfabeton, la ofseto estas nul, do ni konservis kongruon kun ASCII.

Same por kodoj postulantaj 3 bajtojn:

  1. Tri bajtoj 110yyyyy yxxxxxxx xxxxxxxx indiki simbolon kun nombro yyyyyy yxxxxxxx xxxxxxxx, ŝanĝi aktuala alfabeto sur yyyyyy y0000000 00000000 (memoris ĉion krom la pli junaj 15a bito), kaj marku la skatolon en kiu ni nun estas longa reĝimo (kiam reŝanĝas la alfabeton al duobla bajta, ni restarigos ĉi tiun flagon);
  2. Du bajtoj 0xxxxxxx xxxxxxxx en longa reĝimo ĝi estas la signo de la nuna alfabeto. Simile, ni aldonas ĝin kun la ofseto de paŝo 1. La sola diferenco estas, ke nun ni legas du bajtojn (ĉar ni ŝanĝis al ĉi tiu reĝimo).

Sonas bone: nun dum ni bezonas kodi signojn de la sama 7-bita Unikoda gamo, ni elspezas 1 kroman bajton komence kaj entute unu bajton per signo.

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Laborante de unu el la pli fruaj versioj. Ĝi jam ofte batas UTF-8, sed ankoraŭ estas loko por plibonigo.

Kio estas pli malbona? Unue, ni havas kondiĉon, nome aktuala alfabeta ofseto kaj markobutono longa reĝimo. Ĉi tio plue limigas nin: nun la samaj signoj povas esti kodigitaj malsame en malsamaj kuntekstoj. Serĉado de subĉenoj, ekzemple, devos esti farita konsiderante tion, kaj ne nur komparante bajtojn. Due, tuj kiam ni ŝanĝis la alfabeton, ĝi malboniĝis kun la kodado de ASCII-signoj (kaj ĉi tio estas ne nur la latina alfabeto, sed ankaŭ baza interpunkcio, inkluzive de spacoj) - ili postulas ŝanĝi la alfabeton denove al 0, tio estas, denove kroman bajton (kaj poste alian por reveni al nia ĉefa punkto).

Unu alfabeto estas bona, du estas pli bona

Ni provu iomete ŝanĝi niajn bitprefiksojn, enpremante unu pli al la tri supre priskribitaj:

0xxxxxxx — 1 bajto en normala reĝimo, 2 en longa reĝimo
11xxxxxx — 1 bajto
100xxxxx xxxxxxxx - 2 bajtoj
101xxxxx xxxxxxxx xxxxxxxx - 3 bajtoj

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8

Nun en du-bajta registro estas unu malpli disponebla bito - kodpunktoj ĝis 0x1FFFkaj ne 0x3FFF. Tamen ĝi ankoraŭ estas rimarkeble pli granda ol en duoblaj bajtaj UTF-8-kodoj, plej oftaj lingvoj ankoraŭ taŭgas, la plej rimarkinda perdo falis. hiragana и katakana, la japanoj estas malgajaj.

Kio estas ĉi tiu nova kodo? 11xxxxxx? Ĉi tio estas malgranda "ŝtofo" de 64 signoj en grandeco, ĝi kompletigas nian ĉefan alfabeton, do mi nomis ĝin helpa (helpa) alfabeto. Kiam ni ŝanĝas la nunan alfabeton, peco de la malnova alfabeto fariĝas helpa. Ekzemple, ni ŝanĝis de ASCII al Cirila - la kaŝejo nun enhavas 64 signojn enhavantajn Latina alfabeto, ciferoj, spaco kaj komo (plej oftaj enmetoj en ne-ASCII tekstoj). Reiru al ASCII - kaj la ĉefa parto de la cirila alfabeto fariĝos la helpa alfabeto.

Danke al aliro al du alfabetoj, ni povas manipuli grandan nombron da tekstoj kun minimumaj kostoj por ŝanĝado de alfabetoj (interpunkcio plej ofte kondukos al reveno al ASCII, sed post tio ni ricevos multajn ne-ASCII-signojn el la aldona alfabeto, sen ŝanĝante denove).

Gratifiko: prefiksado de la subalfabeto 11xxxxxx kaj elektante ĝian komencan ofseton por esti 0xC0, ni ricevas partan kongruon kun CP1252. Alivorte, multaj (sed ne ĉiuj) okcidenteŭropaj tekstoj koditaj en CP1252 aspektos same en UTF-C.

Ĉi tie tamen aperas malfacilaĵo: kiel akiri helpan el la ĉefa alfabeto? Vi povas lasi la saman ofseton, sed - ve - ĉi tie la Unikoda strukturo jam ludas kontraŭ ni. Tre ofte la ĉefa parto de la alfabeto ne estas komence de la bloko (ekzemple, la rusa majusklo "A" havas la kodon 0x0410, kvankam la cirila bloko komenciĝas per 0x0400). Tiel, preninte la unuajn 64 signojn en la kaŝejon, ni eble perdos la aliron al la vosta parto de la alfabeto.

Por solvi ĉi tiun problemon, mi permane ekzamenis kelkajn blokojn respondajn al malsamaj lingvoj, kaj specifis la ofseton de la helpalfabeto ene de la ĉefa por ili. La latina alfabeto, kiel escepto, estis ĝenerale reordigita kiel bazo64.

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8

Finaj tuŝoj

Ni finfine pensu pri kie alie ni povas plibonigi ion.

Notu ke la formato 101xxxxx xxxxxxxx xxxxxxxx permesas vin kodi nombrojn ĝis 0x1FFFFF, kaj Unikodo finiĝas pli frue, je 0x10FFFF. Alivorte, la lasta kodpunkto estos reprezentita kiel 10110000 11111111 11111111. Tial, ni povas diri ke se la unua bajto estas de la formo 1011xxxx (kie xxxx pli granda ol 0), tiam ĝi signifas ion alian. Ekzemple, vi povas aldoni pliajn 15 signojn tie, kiuj estas konstante disponeblaj por kodado per unu bajto, sed mi decidis fari ĝin alimaniere.

Ni rigardu tiujn Unikodajn blokojn, kiuj postulas nun tri bajtojn. Esence, kiel jam menciite, ĉi tiuj estas ĉinaj signoj - sed estas malfacile fari ion per ili, estas 21 mil da ili. Sed ankaŭ hiragana kaj katakana flugis tien — kaj ne estas plu tiom da ili, malpli ol ducent. Kaj, ĉar ni memoris la japanojn, estas ankaŭ emojis (fakte, ili estas disaj en multaj lokoj en Unikodo, sed la ĉefaj blokoj estas en la gamo. 0x1F300 - 0x1FBFF). Se vi pensas pri tio, ke nun estas emojioj, kiuj estas kunvenitaj de pluraj kodpunktoj samtempe (ekzemple, la emoji ‍‍‍Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8 konsistas el eĉ 7 kodoj!), tiam fariĝas tute domaĝe elspezi tri bajtojn por ĉiu (7×3 = 21 bajtoj pro unu ikono, koŝmaro).

Tial ni elektas kelkajn elektitajn gamojn respondajn al emoji, hiragana kaj katakana, renumeras ilin en unu kontinuan liston kaj kodas ilin kiel du bajtojn anstataŭ tri:

1011xxxx xxxxxxxx

Bonege: la menciita ‍‍‍ emojiAlia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8, konsistanta el 7 kodpunktoj, prenas 8 bajtojn en UTF-25, kaj ni enĝustigas ĝin 14 (precize du bajtoj por ĉiu kodpunkto). Cetere, Habr rifuzis digesti ĝin (kaj en la malnova kaj en la nova redaktilo), do mi devis enmeti ĝin kun bildo.

Ni provu ripari unu plian problemon. Kiel ni memoras, la baza alfabeto estas esence alta 6 bitoj, kiun ni memoras kaj gluas al la kodo de ĉiu sekva malkodita simbolo. En la kazo de ĉinaj signoj kiuj estas en la bloko 0x4E00 - 0x9FFF, ĉi tio estas aŭ bito 0 aŭ 1. Ĉi tio ne estas tre oportuna: ni devos konstante ŝanĝi la alfabeton inter ĉi tiuj du valoroj (t.e. elspezi tri bajtojn). Sed rimarku, ke en la longa reĝimo, de la kodo mem ni povas subtrahi la nombron da signoj, kiujn ni kodas per la mallonga reĝimo (post ĉiuj lertaĵoj priskribitaj supre, ĉi tio estas 10240) - tiam la gamo de hieroglifoj ŝanĝiĝos al 0x2600 - 0x77FF, kaj ĉi-kaze, tra ĉi tiu tuta gamo, la plej signifaj 6 bitoj (el 21) estos egala al 0. Tiel, sekvencoj de hieroglifoj uzos du bajtojn per hieroglifo (kio estas optimuma por tia granda gamo), sen kaŭzante alfabetajn ŝaltilojn.

Alternativaj solvoj: SCSU, BOCU-1

Unikodaj fakuloj, ĵus leginte la titolon de la artikolo, plej verŝajne rapidos memorigi al vi, ke rekte inter la Unikodaj normoj estas Norma Kunprema Skemo por Unikodo (SCSU), kiu priskribas kodigan metodon tre similan al tiu priskribita en la artikolo.

Mi konfesas honeste: mi eksciis pri ĝia ekzisto nur post kiam mi profunde enprofundiĝis en la skribadon de mia decido. Se mi scius pri tio de la komenco, mi verŝajne estus provinta verki efektivigon anstataŭ elpensi mian propran aliron.

Interese estas, ke SCSU uzas ideojn tre similajn al tiuj, kiujn mi mem elpensis (anstataŭ la koncepto de "alfabetoj" ili uzas "fenestrojn", kaj estas pli da ili disponeblaj ol mi havas). Samtempe, ĉi tiu formato ankaŭ havas malavantaĝojn: ĝi estas iom pli proksima al kunpremaj algoritmoj ol kodigaj. Precipe, la normo donas multajn reprezentajn metodojn, sed ne diras kiel elekti la optimuman - por tio, la kodilo devas uzi ian heŭristiko. Tiel, SCSU-kodilo kiu produktas bonan pakadon estos pli kompleksa kaj pli maloportuna ol mia algoritmo.

Por komparo, mi transdonis relative simplan efektivigon de SCSU al JavaScript - laŭ kodvolumo ĝi montriĝis komparebla al mia UTF-C, sed en kelkaj kazoj la rezulto estis dekoj de procentoj pli malbona (foje ĝi povas superi ĝin, sed ne multe). Ekzemple, tekstoj en la hebrea kaj la greka estis ĉifritaj per UTF-C 60% pli bona ol SCSU (verŝajne pro iliaj kompaktaj alfabetoj).

Aparte, mi aldonos, ke krom SCSU ekzistas ankaŭ alia maniero kompakte reprezenti Unikodon - BOCU-1, sed ĝi celas MIME-kongruon (kiun mi ne bezonis) kaj prenas iomete alian aliron al kodado. Mi ne taksis ĝian efikecon, sed ŝajnas al mi, ke ĝi verŝajne ne estas pli alta ol SCSU.

Eblaj plibonigoj

La algoritmo, kiun mi prezentis, ne estas universala laŭ dezajno (tio estas verŝajne kie miaj celoj plej diverĝas de la celoj de la Unikoda Konsorcio). Mi jam menciis, ke ĝi estis evoluigita ĉefe por unu tasko (stoki multlingvan vortaron en prefiksa arbo), kaj kelkaj el ĝiaj trajtoj eble ne bone taŭgas por aliaj taskoj. Sed la fakto, ke ĝi ne estas normo, povas esti pluso - vi povas facile modifi ĝin laŭ viaj bezonoj.

Ekzemple, en la evidenta maniero vi povas forigi la ĉeeston de ŝtato, fari sennacian kodigon - simple ne ĝisdatigu variablojn offs, auxOffs и is21Bit en la kodilo kaj malĉifrilo. En ĉi tiu kazo, ne eblos efike paki sekvencojn de signoj de la sama alfabeto, sed estos garantio, ke la sama signo ĉiam estas kodita per la samaj bajtoj, sendepende de la kunteksto.

Krome, vi povas adapti la kodilon al specifa lingvo ŝanĝante la defaŭltan staton - ekzemple, fokusante rusajn tekstojn, agordu la kodilon kaj malĉifrilon komence. offs = 0x0400 и auxOffs = 0. Ĉi tio precipe havas sencon en la kazo de sennacia reĝimo. Ĝenerale, tio estos simila al uzado de la malnova ok-bita kodigo, sed sen forigi la eblon enmeti signojn de ĉiuj Unikodoj laŭbezone.

Alia malavantaĝo menciita antaŭe estas, ke en granda teksto kodita en UTF-C ne ekzistas rapida maniero trovi la signolimon plej proksiman al arbitra bajto. Se vi fortranĉas la lastajn, ekzemple, 100 bajtojn de la kodita bufro, vi riskas ricevi rubon per kiu vi nenion povas fari. La kodigo ne estas destinita por stoki plurgigabajtajn protokolojn, sed ĝenerale tio povas esti korektita. Byte 0xBF neniam devas aperi kiel la unua bajto (sed povas esti la dua aŭ tria). Tial, dum kodado, vi povas enmeti la sekvencon 0xBF 0xBF 0xBF ĉiu, ekzemple, 10 KB - tiam, se vi bezonas trovi limon, sufiĉos skani la elektitan pecon ĝis trovi simila markilo. Sekvante la lastan 0xBF estas garantiita esti la komenco de karaktero. (Dum malkodado, ĉi tiu sekvenco de tri bajtoj, kompreneble, devos esti ignorita.)

Resumi

Se vi legis ĉi tie, gratulon! Mi esperas, ke vi, kiel mi, lernis ion novan (aŭ refreŝigis vian memoron) pri la strukturo de Unikodo.

Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8
Demopaĝo. La ekzemplo de la hebrea montras la avantaĝojn super kaj UTF-8 kaj SCSU.

La supre priskribita esplorado ne devus esti konsiderata kiel trudado de normoj. Tamen mi estas ĝenerale kontenta pri la rezultoj de mia laboro, do mi estas kontenta pri ili dividi: ekzemple, minimumigita JS-biblioteko pezas nur 1710 bajtojn (kaj ne havas dependecojn, kompreneble). Kiel mi menciis supre, ŝia laboro troveblas ĉe demo paĝo (estas ankaŭ aro da tekstoj, sur kiuj oni povas ĝin kompari kun UTF-8 kaj SCSU).

Fine, mi denove atentigos kazojn en kiuj UTF-C estas uzata Ne valoras ĝin:

  • Se viaj linioj estas sufiĉe longaj (de 100-200 signoj). En ĉi tiu kazo, vi devus pensi pri uzado de kunpremaj algoritmoj kiel deflate.
  • Se vi bezonas ASCII-travidebleco, tio estas, estas grave por vi, ke la koditaj sekvencoj ne enhavas ASCII-kodojn kiuj ne estis en la origina ĉeno. La bezono de tio povas esti evitita se, interagante kun triaj API-oj (ekzemple, laborante kun datumbazo), vi pasas la kodigan rezulton kiel abstraktan aron de bajtoj, kaj ne kiel ĉenojn. Alie, vi riskas ricevi neatenditajn vundeblecojn.
  • Se vi volas povi rapide trovi signajn limojn ĉe arbitra ofseto (ekzemple, kiam parto de linio estas difektita). Ĉi tio povas esti farita, sed nur skanante la linion de la komenco (aŭ aplikante la modifon priskribitan en la antaŭa sekcio).
  • Se vi bezonas rapide fari operaciojn pri la enhavo de ĉenoj (ordigu ilin, serĉu subŝnurojn en ili, kunkatenigi). Ĉi tio postulas unue malkoditajn ŝnurojn, do UTF-C estos pli malrapida ol UTF-8 en ĉi tiuj kazoj (sed pli rapide ol kunpremaj algoritmoj). Ĉar la sama ĉeno ĉiam estas ĉifrita laŭ la saman manieron, preciza komparo de malkodado ne estas postulata kaj povas esti farita sur bajto-post-bajta bazo.

ĝisdatigo: la uzanto Tiomiĉ en la komentoj sube afiŝis grafeon elstarigante la aplikebleclimojn de UTF-C. Ĝi montras ke UTF-C estas pli efika ol ĝeneraluzebla kunpremadalgoritmo (vario de LZW) kondiĉe ke la plenplena ŝnuro estas pli mallonga. ~140 signoj (tamen mi rimarkas, ke la komparo estis farita sur unu teksto; por aliaj lingvoj la rezulto povas malsami).
Alia biciklo: ni stokas Unikodajn ŝnurojn 30-60% pli kompaktajn ol UTF-8

fonto: www.habr.com

Aldoni komenton