Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8

Si sou un desenvolupador i us enfronteu a la tasca d'escollir una codificació, Unicode gairebé sempre serà la solució adequada. El mètode de representació específic depèn del context, però la majoria de vegades també hi ha una resposta universal: UTF-8. El millor és que us permet utilitzar tots els caràcters Unicode sense gastar massa molts bytes en la majoria dels casos. És cert que per a les llengües que utilitzen més que l'alfabet llatí, "no massa" és almenys dos bytes per caràcter. Podem fer-ho millor sense tornar a les codificacions prehistòriques que ens limiten a només 256 caràcters disponibles?

A continuació us proposo familiaritzar-vos amb el meu intent de respondre aquesta pregunta i implementar un algorisme relativament senzill que us permet emmagatzemar línies en la majoria d'idiomes del món sense afegir la redundància que hi ha a UTF-8.

Exempció de responsabilitat. Immediatament faré algunes reserves importants: la solució descrita no s'ofereix com a reemplaçament universal d'UTF-8, només és adequat en una llista reduïda de casos (més sobre ells a continuació), i en cap cas s'ha d'utilitzar per interactuar amb API de tercers (que ni tan sols en coneixen). Molt sovint, els algorismes de compressió de propòsit general (per exemple, desinflar) són adequats per a l'emmagatzematge compacte de grans volums de dades de text. A més, ja en el procés de crear la meva solució, vaig trobar un estàndard existent al mateix Unicode, que resol el mateix problema: és una mica més complicat (i sovint pitjor), però encara és un estàndard acceptat i no només junts al genoll. Jo també us parlaré d'ell.

Sobre Unicode i UTF-8

Per començar, unes paraules sobre què és Unicode и UTF-8.

Com sabeu, les codificacions de 8 bits solien ser populars. Amb ells, tot era senzill: es poden numerar 256 caràcters amb números del 0 al 255 i, òbviament, els números del 0 al 255 es poden representar com un byte. Si tornem al principi, la codificació ASCII està completament limitada a 7 bits, de manera que el bit més significatiu en la seva representació de bytes és zero, i la majoria de codificacions de 8 bits són compatibles amb ella (només es diferencien en la "superior" part, on el bit més significatiu és un).

En què difereix Unicode d'aquestes codificacions i per què hi ha tantes representacions específiques associades: UTF-8, UTF-16 (BE i LE), UTF-32? Anem a ordenar-ho.

L'estàndard Unicode bàsic només descriu la correspondència entre caràcters (i, en alguns casos, components individuals dels caràcters) i els seus números. I hi ha molts números possibles en aquest estàndard: des de 0x00 до 0x10FFFF (1 peces). Si volguéssim posar un nombre d'aquest rang en una variable, ni 114 ni 112 bytes ens serien suficients. I com que els nostres processadors no estan molt dissenyats per treballar amb números de tres bytes, ens veurem obligats a utilitzar fins a 1 bytes per caràcter! Això és UTF-2, però és precisament per aquest "malbaratament" que aquest format no és popular.

Afortunadament, l'ordre dels caràcters dins d'Unicode no és aleatori. Tot el seu conjunt està dividit en 17 "avions", cadascun dels quals conté 65536 (0x10000) «punts de codi" El concepte d'un "punt de codi" aquí és senzill número de caràcter, que li ha assignat Unicode. Però, com s'ha esmentat anteriorment, a Unicode no només es numeren els caràcters individuals, sinó també els seus components i marques de servei (i de vegades res no correspon al nombre, potser de moment, però per a nosaltres això no és tan important), així que és més correcte parlar sempre específicament del nombre de nombres en si, i no dels símbols. Tanmateix, a continuació, per motius de brevetat, utilitzaré sovint la paraula "símbol", implicant el terme "punt de codi".

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Plans Unicode. Com podeu veure, la major part (plans 4 a 13) encara està sense utilitzar.

El més notable és que tota la "polpa" principal es troba en el pla zero, s'anomena "Pla Bàsic Multilingüe". Si una línia conté text en un dels idiomes moderns (inclòs el xinès), no aniràs més enllà d'aquest pla. Però tampoc no pots tallar la resta d'Unicode; per exemple, els emoji es troben principalment al final de el següent avió",Pla Suplementari Multilingüe"(s'estén des de 0x10000 до 0x1FFFF). Així doncs, UTF-16 fa això: tots els caràcters que hi pertanyen Pla Bàsic Multilingüe, es codifiquen "tal qual" amb un número de dos bytes corresponent. Tanmateix, alguns dels números d'aquest rang no indiquen caràcters específics, però indiquen que després d'aquest parell de bytes hem de considerar-ne un altre: combinant els valors d'aquests quatre bytes, obtenim un nombre que cobreix tot l'interval Unicode vàlid. Aquesta idea s'anomena "parelles substitutes"; és possible que n'hagueu sentit a parlar.

Per tant, UTF-16 requereix dos o (en casos molt rars) quatre bytes per "punt de codi". Això és millor que utilitzar quatre bytes tot el temps, però el llatí (i altres caràcters ASCII) quan es codifica d'aquesta manera malgasta la meitat de l'espai en zeros. UTF-8 està dissenyat per corregir-ho: ASCII ocupa, com abans, només un byte; codis de 0x80 до 0x7FF - dos bytes; des de 0x800 до 0xFFFF - tres, i de 0x10000 до 0x10FFFF - quatre. D'una banda, l'alfabet llatí s'ha tornat bo: ha tornat la compatibilitat amb ASCII i la distribució s'està "estén" més uniformement d'1 a 4 bytes. Però, per desgràcia, els alfabets diferents del llatí no es beneficien de cap manera en comparació amb UTF-16, i molts ara requereixen tres bytes en lloc de dos; l'interval cobert per un registre de dos bytes s'ha reduït 32 vegades, amb 0xFFFF до 0x7FF, i ni el xinès ni, per exemple, el georgià s'hi inclouen. Ciríl·lic i cinc alfabets més - hurra - afortunat, 2 bytes per caràcter.

Per què passa això? Vegem com UTF-8 representa els codis de caràcters:
Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Directament per representar números, aquí s'utilitzen bits marcats amb el símbol x. Es pot veure que en un registre de dos bytes només hi ha 11 bits (de 16). Els bits principals aquí només tenen una funció auxiliar. En el cas d'un registre de quatre bytes, s'assignen 21 de 32 bits per al número de punt de codi; sembla que n'hi ha prou amb tres bytes (que donen un total de 24 bits), però els marcadors de servei mengen massa.

Això és dolent? No realment. D'una banda, si ens importa molt l'espai, tenim algorismes de compressió que poden eliminar fàcilment tota l'entropia i la redundància addicionals. D'altra banda, l'objectiu d'Unicode era proporcionar la codificació més universal possible. Per exemple, podem confiar una línia codificada en UTF-8 a un codi que abans només funcionava amb ASCII, i no tenir por que veurà un caràcter de l'interval ASCII que realment no hi és (al cap i a la fi, en UTF-8 tot). bytes que comencen amb el bit zero; això és exactament el que és ASCII). I si de sobte volem tallar una petita cua d'una cadena gran sense descodificar-la des del principi (o restaurar part de la informació després d'una secció danyada), ens és fàcil trobar el desplaçament on comença un personatge (n'hi ha prou per saltar bytes que tenen un prefix de bits 10).

Aleshores, per què inventar alguna cosa nova?

Al mateix temps, de vegades hi ha situacions en què els algorismes de compressió com el desinflat són poc aplicables, però voleu aconseguir un emmagatzematge compacte de cadenes. Personalment, em vaig trobar amb aquest problema quan pensava en construir arbre de prefixos comprimit per a un gran diccionari que inclou paraules en idiomes arbitraris. D'una banda, cada paraula és molt curta, de manera que comprimir-la serà ineficaç. D'altra banda, la implementació de l'arbre que vaig considerar es va dissenyar de manera que cada byte de la cadena emmagatzemada generava un vèrtex d'arbre independent, de manera que minimitzar-ne el nombre era molt útil. A la meva biblioteca Az.js (Com a pimorfia 2, en què es basa) un problema similar es pot resoldre simplement: cadenes empaquetades DAWG-diccionari, emmagatzemat allà bon vell CP1251. Però, com és fàcil d'entendre, això només funciona bé per a un alfabet limitat: no es pot afegir una línia en xinès a aquest diccionari.

Per separat, m'agradaria assenyalar un matís més desagradable que sorgeix quan s'utilitza UTF-8 en aquesta estructura de dades. La imatge de dalt mostra que quan un caràcter s'escriu com a dos bytes, els bits relacionats amb el seu nombre no vénen en fila, sinó que estan separats per un parell de bits. 10 al mig: 110xxxxx 10xxxxxx. Per això, quan els 6 bits inferiors del segon byte desborden el codi de caràcters (és a dir, es produeix una transició 1011111110000000), llavors el primer byte també canvia. Resulta que la lletra "p" es denota amb bytes 0xD0 0xBF, i la següent "r" ja és 0xD1 0x80. En un arbre de prefixos, això condueix a la divisió del node pare en dos: un per al prefix 0xD0, i un altre per 0xD1 (tot i que tot l'alfabet ciríl·lic només es podria codificar pel segon byte).

Què he aconseguit

Davant d'aquest problema, vaig decidir practicar jocs amb bits i, al mateix temps, familiaritzar-me una mica millor amb l'estructura d'Unicode en conjunt. El resultat va ser el format de codificació UTF-C ("C" per a compacte), que no gasta més de 3 bytes per punt de codi i, molt sovint, només us permet gastar un byte addicional per a tota la línia codificada. Això porta al fet que en molts alfabets no ASCII aquesta codificació resulta ser 30-60% més compacte que UTF-8.

He presentat exemples d'implementació d'algoritmes de codificació i descodificació en forma Biblioteques JavaScript i Go, podeu utilitzar-los lliurement al vostre codi. Però encara subratllaré que, en certa manera, aquest format segueix sent una "bicicleta", i no recomano utilitzar-lo. sense adonar-se de per què ho necessites. Això encara és més un experiment que una seriosa "millora d'UTF-8". No obstant això, el codi està escrit de manera ordenada, concisa, amb un gran nombre de comentaris i cobertura de proves.

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Resultats de la prova i comparació amb UTF-8

Jo també ho vaig fer pàgina de demostració, on podeu avaluar el rendiment de l'algorisme, i després us explicaré més sobre els seus principis i el procés de desenvolupament.

Eliminació de bits redundants

Vaig prendre UTF-8 com a base, és clar. El primer i més evident que es pot canviar és reduir el nombre de bits de servei en cada byte. Per exemple, el primer byte en UTF-8 sempre comença amb qualsevol 0, o amb 11 - un prefix 10 Només el tenen els bytes següents. Substituïm el prefix 11 en 1, i per als propers bytes eliminarem els prefixos completament. Què passarà?

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

Espera, on és el registre de quatre bytes? Però ja no és necessari: quan escrivim en tres bytes, ara tenim 21 bits disponibles i això és suficient per a tots els números fins a 0x10FFFF.

Què hem sacrificat aquí? El més important és la detecció de límits de caràcters des d'una ubicació arbitrària a la memòria intermèdia. No podem apuntar a un byte arbitrari i trobar-ne el començament del caràcter següent. Aquesta és una limitació del nostre format, però a la pràctica rarament és necessari. Normalment som capaços de passar pel buffer des del principi (especialment quan es tracta de línies curtes).

La situació de cobrir idiomes amb 2 bytes també ha millorat: ara el format de dos bytes ofereix un rang de 14 bits, i aquests són codis fins a 0x3FFF. Els xinesos tenen mala sort (els seus caràcters van des de 0x4E00 до 0x9FFF), però els georgians i molts altres pobles es diverteixen més: les seves llengües també encaixen en 2 bytes per caràcter.

Introduïu l'estat del codificador

Pensem ara en les propietats de les pròpies línies. El diccionari conté més sovint paraules escrites en caràcters del mateix alfabet, i això també és cert per a molts altres textos. Seria bo indicar aquest alfabet una vegada i després indicar només el número de la lletra que hi ha dins. A veure si la disposició dels caràcters a la taula Unicode ens ajudarà.

Com s'ha esmentat anteriorment, Unicode es divideix en avió 65536 codis cadascun. Però aquesta no és una divisió molt útil (com ja s'ha dit, la majoria de vegades estem en el pla zero). Més interessant és la divisió per blocs. Aquests intervals ja no tenen una longitud fixa i són més significatius: per regla general, cadascun combina caràcters del mateix alfabet.

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Un bloc que conté caràcters de l'alfabet bengalí. Malauradament, per raons històriques, aquest és un exemple d'embalatge poc dens: 96 caràcters estan dispersos de manera caòtica en 128 punts de codi de bloc.

Els inicis dels blocs i les seves mides són sempre múltiples de 16; això es fa simplement per comoditat. A més, molts blocs comencen i acaben en valors que són múltiples de 128 o fins i tot de 256; per exemple, l'alfabet ciríl·lic bàsic ocupa 256 bytes de 0x0400 до 0x04FF. Això és força convenient: si desem el prefix una vegada 0x04, llavors qualsevol caràcter ciríl·lic es pot escriure en un byte. És cert que així perdrem l'oportunitat de tornar a ASCII (i a qualsevol altre personatge en general). Per tant fem això:

  1. Dos bytes 10yyyyyy yxxxxxxx no només denoten un símbol amb un nombre yyyyyy yxxxxxxx, però també canvia alfabet actual en yyyyyy y0000000 (és a dir, recordem tots els bits excepte els menys significatius 7 bit);
  2. Un byte 0xxxxxxx aquest és el caràcter de l'alfabet actual. Només cal afegir-lo al desplaçament que vam recordar al pas 1. Tot i que no hem canviat l'alfabet, el desplaçament és zero, de manera que hem mantingut la compatibilitat amb ASCII.

De la mateixa manera per als codis que requereixen 3 bytes:

  1. Tres bytes 110yyyyy yxxxxxxx xxxxxxxx indica un símbol amb un número yyyyyy yxxxxxxx xxxxxxxx, canvi alfabet actual en yyyyyy y0000000 00000000 (recordava tot excepte els més joves 15 bit), i marqueu la casella en què estem ara llarg mode (quan tornem a canviar l'alfabet a un de doble byte, restablirem aquesta bandera);
  2. Dos bytes 0xxxxxxx xxxxxxxx en mode llarg és el caràcter de l'alfabet actual. De la mateixa manera, l'afegim amb el desplaçament del pas 1. L'única diferència és que ara llegim dos bytes (perquè hem canviat a aquest mode).

Sona bé: ara, tot i que necessitem codificar caràcters del mateix rang Unicode de 7 bits, gastem 1 byte addicional al principi i un total d'un byte per caràcter.

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Treballant a partir d'una de les versions anteriors. Ja supera sovint UTF-8, però encara hi ha marge per millorar.

Què és pitjor? En primer lloc, tenim una condició, és a dir desplaçament de l'alfabet actual i casella de selecció mode llarg. Això ens limita encara més: ara els mateixos caràcters es poden codificar de manera diferent en diferents contextos. La cerca de subcadenes, per exemple, s'haurà de fer tenint-ho en compte, i no només comparant bytes. En segon lloc, tan bon punt vam canviar l'alfabet, es va tornar malament amb la codificació de caràcters ASCII (i això no és només l'alfabet llatí, sinó també la puntuació bàsica, inclosos els espais) - requereixen canviar l'alfabet de nou a 0, és a dir, de nou un byte addicional (i després un altre per tornar al nostre punt principal).

Un alfabet és bo, dos és millor

Intentem canviar una mica els nostres prefixos de bits, apretant-ne un més als tres descrits anteriorment:

0xxxxxxx — 1 byte en mode normal, 2 en mode llarg
11xxxxxx - 1 byte
100xxxxx xxxxxxxx - 2 bytes
101xxxxx xxxxxxxx xxxxxxxx - 3 bytes

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8

Ara, en un registre de dos bytes, hi ha un bit disponible menys: punts de codi fins 0x1FFFI no 0x3FFF. No obstant això, encara és notablement més gran que en els codis UTF-8 de doble byte, els idiomes més comuns encara encaixen, la pèrdua més notable ha caigut. hiragana и katakana, els japonesos estan tristos.

Què és aquest nou codi? 11xxxxxx? Es tracta d'un petit "emmagatzematge" de 64 caràcters de mida, que complementa el nostre alfabet principal, així que el vaig anomenar auxiliar (auxiliar) alfabet. Quan canviem l'alfabet actual, un tros de l'antic alfabet esdevé auxiliar. Per exemple, hem canviat d'ASCII a ciríl·lic: ara l'emmagatzematge conté 64 caràcters que contenen Alfabet llatí, nombres, espai i coma (insercions més freqüents en textos no ASCII). Torneu a ASCII i la part principal de l'alfabet ciríl·lic es convertirà en l'alfabet auxiliar.

Gràcies a l'accés a dos alfabets, podem gestionar un gran nombre de textos amb uns costos mínims per canviar d'alfabet (la puntuació conduirà sovint a un retorn a ASCII, però després obtindrem molts caràcters no ASCII de l'alfabet addicional, sense canviant de nou).

Bonificació: prefixant el subalfabet 11xxxxxx i escollint el seu desplaçament inicial 0xC0, obtenim una compatibilitat parcial amb CP1252. En altres paraules, molts (però no tots) els textos d'Europa occidental codificats a CP1252 tindran el mateix aspecte en UTF-C.

Aquí, però, sorgeix una dificultat: com obtenir un auxiliar de l'alfabet principal? Podeu deixar el mateix desplaçament, però, per desgràcia, aquí l'estructura Unicode ja juga contra nosaltres. Molt sovint, la part principal de l'alfabet no es troba al començament del bloc (per exemple, la capital russa "A" té el codi 0x0410, encara que el bloc ciríl·lic comença per 0x0400). Així, després d'haver introduït els primers 64 caràcters a l'emmagatzematge, és possible que perdem l'accés a la part final de l'alfabet.

Per solucionar aquest problema, vaig recórrer manualment alguns blocs corresponents a diferents idiomes i vaig especificar el desplaçament de l'alfabet auxiliar dins del principal per a ells. L'alfabet llatí, com a excepció, es reordenava generalment com a base64.

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8

Tocs finals

Per fi pensem en on més podem millorar alguna cosa.

Tingueu en compte que el format 101xxxxx xxxxxxxx xxxxxxxx permet codificar números fins a 0x1FFFFF, i Unicode acaba abans, a les 0x10FFFF. En altres paraules, l'últim punt de codi es representarà com 10110000 11111111 11111111. Per tant, podem dir que si el primer byte és de la forma 1011xxxx (On xxxx superior a 0), aleshores significa una altra cosa. Per exemple, podeu afegir-hi 15 caràcters més que estan constantment disponibles per a la codificació en un byte, però vaig decidir fer-ho d'una altra manera.

Mirem aquells blocs Unicode que requereixen ara tres bytes. Bàsicament, com ja s'ha esmentat, es tracta de caràcters xinesos, però és difícil fer-hi res, n'hi ha 21 mil. Però hiragana i katakana també hi van volar, i ja no n'hi ha tants, menys de dos-cents. I, com que recordem els japonesos, també hi ha emojis (de fet, estan escampats per molts llocs a Unicode, però els blocs principals estan dins el rang). 0x1F300 - 0x1FBFF). Si penseu en el fet que ara hi ha emojis que s'agrupen des de diversos punts de codi alhora (per exemple, l'emoji ‍‍‍Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8 consta de fins a 7 codis!), llavors és una vergonya gastar tres bytes en cadascun (7×3 = 21 bytes pel bé d'una icona, un malson).

Per tant, seleccionem uns quants rangs seleccionats corresponents a emoji, hiragana i katakana, els renumerem en una llista contínua i els codifiquem com a dos bytes en lloc de tres:

1011xxxx xxxxxxxx

Genial: l'emoji esmentat anteriormentUna altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8, que consta de 7 punts de codi, pren 8 bytes en UTF-25 i l'ajustem 14 (exactament dos bytes per a cada punt de codi). Per cert, Habr es va negar a digerir-lo (tant al vell com al nou), així que l'he hagut d'inserir amb una imatge.

Intentem solucionar un problema més. Com recordem, l'alfabet bàsic és essencialment alt 6 bits, que tenim present i enganxem al codi de cada següent símbol descodificat. En el cas dels caràcters xinesos que es troben al bloc 0x4E00 - 0x9FFF, això és el bit 0 o 1. Això no és molt convenient: haurem de canviar constantment l'alfabet entre aquests dos valors (és a dir, gastar tres bytes). Però tingueu en compte que en el mode llarg, del codi en si podem restar el nombre de caràcters que codifiquem mitjançant el mode curt (després de tots els trucs descrits anteriorment, aquest és 10240), llavors el rang de jeroglífics canviarà a 0x2600 - 0x77FF, i en aquest cas, en tot aquest rang, els 6 bits més significatius (de 21) seran iguals a 0. Així, les seqüències de jeroglífics utilitzaran dos bytes per jeroglífic (el que és òptim per a un rang tan gran), sense provocant canvis alfabètics.

Solucions alternatives: SCSU, BOCU-1

Els experts en Unicode, després de llegir el títol de l'article, probablement s'afanyin a recordar-vos que directament entre els estàndards Unicode hi ha Esquema de compressió estàndard per a Unicode (SCSU), que descriu un mètode de codificació molt similar al descrit a l'article.

Admeto sincerament: només vaig saber de la seva existència després d'estar profundament immers en escriure la meva decisió. Si ho hagués sabut des del principi, probablement hauria intentat escriure una implementació en lloc d'aconseguir el meu propi enfocament.

El que és interessant és que SCSU fa servir idees molt semblants a les que vaig plantejar jo sola (en comptes del concepte d'"alfabets", fan servir "finestres", i n'hi ha més disponibles que jo). Al mateix temps, aquest format també té desavantatges: s'acosta una mica més als algorismes de compressió que als de codificació. En particular, l'estàndard ofereix molts mètodes de representació, però no diu com triar l'òptim; per això, el codificador ha d'utilitzar algun tipus d'heurística. Per tant, un codificador SCSU que produeix un bon embalatge serà més complex i més complicat que el meu algorisme.

Per comparar, vaig transferir una implementació relativament senzilla de SCSU a JavaScript; en termes de volum de codi, va resultar ser comparable al meu UTF-C, però en alguns casos el resultat va ser desenes per cent pitjor (de vegades pot superar-lo, però no gaire). Per exemple, els textos en hebreu i grec van ser codificats per UTF-C 60% millor que SCSU (probablement pels seus alfabets compactes).

Per separat, afegiré que, a més de SCSU, també hi ha una altra manera de representar Unicode de manera compacta: BOCU-1, però té com a objectiu la compatibilitat MIME (que no necessitava) i adopta un enfocament lleugerament diferent de la codificació. No n'he avaluat l'eficàcia, però em sembla que és poc probable que sigui superior a SCSU.

Possibles millores

L'algoritme que vaig presentar no és universal per disseny (és probablement aquí on els meus objectius divergeixen més dels objectius del Consorci Unicode). Ja he esmentat que s'ha desenvolupat principalment per a una tasca (emmagatzemar un diccionari multilingüe en un arbre de prefixos), i algunes de les seves característiques poden no ser molt adequades per a altres tasques. Però el fet que no sigui un estàndard pot ser un avantatge: podeu modificar-lo fàcilment segons les vostres necessitats.

Per exemple, de la manera òbvia, podeu desfer-vos de la presència de l'estat, fer codificació sense estat; simplement no actualitzeu les variables. offs, auxOffs и is21Bit en el codificador i el descodificador. En aquest cas, no serà possible empaquetar de manera efectiva seqüències de caràcters del mateix alfabet, però hi haurà una garantia que el mateix caràcter sempre es codificarà amb els mateixos bytes, independentment del context.

A més, podeu adaptar el codificador a un idioma específic canviant l'estat predeterminat; per exemple, centrant-vos en els textos en rus, establiu el codificador i el descodificador al principi. offs = 0x0400 и auxOffs = 0. Això té sentit especialment en el cas del mode sense estat. En general, això serà similar a utilitzar l'antiga codificació de vuit bits, però sense eliminar la possibilitat d'inserir caràcters de tot Unicode segons sigui necessari.

Un altre inconvenient esmentat anteriorment és que en el text gran codificat en UTF-C no hi ha una manera ràpida de trobar el límit de caràcters més proper a un byte arbitrari. Si talleu els últims, per exemple, 100 bytes de la memòria intermèdia codificada, correu el risc de rebre escombraries amb les quals no podeu fer res. La codificació no està dissenyada per emmagatzemar registres de diversos gigabytes, però en general això es pot corregir. Byte 0xBF mai ha d'aparèixer com el primer byte (però pot ser el segon o tercer). Per tant, en codificar, podeu inserir la seqüència 0xBF 0xBF 0xBF cada, per exemple, 10 KB; llavors, si necessiteu trobar un límit, n'hi haurà prou amb escanejar la peça seleccionada fins que es trobi un marcador similar. Seguint l'últim 0xBF es garanteix que és el començament d'un personatge. (Quan es descodifiqueu, aquesta seqüència de tres bytes, per descomptat, caldrà ignorar-la.)

En resum

Si has llegit fins aquí, enhorabona! Espero que, com jo, hàgiu après alguna cosa nova (o us hàgiu refrescat la memòria) sobre l'estructura d'Unicode.

Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8
Pàgina de demostració. L'exemple de l'hebreu mostra els avantatges tant respecte a UTF-8 com a SCSU.

La investigació descrita anteriorment no s'ha de considerar una invasió dels estàndards. Tanmateix, en general estic satisfet amb els resultats del meu treball, així que estic content amb ells Compartir: per exemple, una biblioteca JS minificada només pesa 1710 bytes (i no té dependències, és clar). Com he esmentat anteriorment, la seva obra es pot trobar a pàgina de demostració (també hi ha un conjunt de textos sobre els quals es pot comparar amb UTF-8 i SCSU).

Finalment, tornaré a cridar l'atenció sobre els casos en què s'utilitza UTF-C no val la pena:

  • Si les teves línies són prou llargues (entre 100 i 200 caràcters). En aquest cas, hauríeu de pensar en utilitzar algorismes de compressió com deflate.
  • Si necessites Transparència ASCII, és a dir, és important per a vostè que les seqüències codificades no continguin codis ASCII que no estiguessin a la cadena original. La necessitat d'això es pot evitar si, quan interactueu amb API de tercers (per exemple, treballant amb una base de dades), passeu el resultat de la codificació com un conjunt abstracte de bytes i no com a cadenes. En cas contrari, corre el risc d'aconseguir vulnerabilitats inesperades.
  • Si voleu poder trobar ràpidament els límits de caràcters amb un desplaçament arbitrari (per exemple, quan una part d'una línia està danyada). Això es pot fer, però només escanejant la línia des del principi (o aplicant la modificació descrita a l'apartat anterior).
  • Si necessiteu realitzar operacions ràpidament sobre el contingut de les cadenes (ordenar-les, cercar-hi subcadenes, concatenar-les). Això requereix que les cadenes es descodifiquen primer, de manera que UTF-C serà més lent que UTF-8 en aquests casos (però més ràpid que els algorismes de compressió). Com que la mateixa cadena sempre està codificada de la mateixa manera, no es requereix una comparació exacta de la descodificació i es pot fer byte per byte.

Actualització: l’usuari Tyomitch als comentaris a continuació va publicar un gràfic que destaca els límits d'aplicabilitat d'UTF-C. Mostra que UTF-C és més eficient que un algorisme de compressió de propòsit general (una variació de LZW) sempre que la cadena empaquetada sigui més curta. ~140 caràcters (no obstant això, noto que la comparació es va dur a terme en un text; per a altres idiomes el resultat pot ser diferent).
Una altra bicicleta: emmagatzemem cadenes Unicode un 30-60% més compactes que UTF-8

Font: www.habr.com

Afegeix comentari