Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8

Se es un programador e estás enfrontado á tarefa de escoller unha codificación, Unicode será case sempre a solución correcta. O método de representación específico depende do contexto, pero a maioría das veces tamén hai unha resposta universal: UTF-8. O bo é que che permite usar todos os caracteres Unicode sen gastar слишком moitos bytes na maioría dos casos. É certo, para as linguas que usan algo máis que o alfabeto latino, "non demasiado" é polo menos dous bytes por carácter. Podemos facelo mellor sen volver ás codificacións prehistóricas que nos limitan a só 256 caracteres dispoñibles?

A continuación propoño familiarizarse co meu intento de responder a esta pregunta e implementar un algoritmo relativamente sinxelo que che permite almacenar liñas na maioría das linguas do mundo sen engadir a redundancia que hai en UTF-8.

Exención de responsabilidade. Inmediatamente farei algunhas reservas importantes: a solución descrita non se ofrece como un substituto universal para UTF-8, só é adecuado nunha lista estreita de casos (máis sobre eles a continuación), e en ningún caso debe usarse para interactuar con API de terceiros (que nin sequera o saben). Na maioría das veces, os algoritmos de compresión de propósito xeral (por exemplo, desinflar) son axeitados para o almacenamento compacto de grandes volumes de datos de texto. Ademais, xa no proceso de creación da miña solución, atopei un estándar existente no propio Unicode, que resolve o mesmo problema: é algo máis complicado (e moitas veces peor), pero aínda así é un estándar aceptado e non só xuntos no xeonllo. Tamén che falarei del.

Acerca de Unicode e UTF-8

Para comezar, unhas palabras sobre o que é Unicode и UTF-8.

Como sabes, as codificacións de 8 bits eran populares. Con eles, todo era sinxelo: 256 caracteres pódense numerar con números do 0 ao 255 e os números do 0 ao 255, obviamente, poden representarse como un byte. Se volvemos ao principio, a codificación ASCII está completamente limitada a 7 bits, polo que o bit máis significativo na súa representación de bytes é cero, e a maioría das codificacións de 8 bits son compatibles con ela (difiren só na "superior" parte, onde o bit máis significativo é un).

En que se diferencia Unicode desas codificacións e por que están asociadas tantas representacións específicas con el: UTF-8, UTF-16 (BE e LE), UTF-32? Imos ordenalo.

O estándar básico de Unicode describe só a correspondencia entre os caracteres (e nalgúns casos, os compoñentes individuais dos caracteres) e os seus números. E hai moitos números posibles neste estándar - de 0x00 para 0x10FFFF (1 pezas). Se queriamos poñer un número de tal rango nunha variable, nin 114 nin 112 bytes serían suficientes para nós. E como os nosos procesadores non están moi deseñados para traballar con números de tres bytes, veriamos obrigados a usar ata 1 bytes por carácter. Este é UTF-2, pero é precisamente por este "despilfarro" polo que este formato non é popular.

Afortunadamente, a orde dos caracteres dentro de Unicode non é aleatoria. Todo o seu conxunto está dividido en 17"avións", cada un dos cales contén 65536 (0x10000) "puntos de código" O concepto de "punto de código" aquí é simplemente número de carácter, asignado por Unicode. Pero, como se mencionou anteriormente, en Unicode non só se numeran os caracteres individuais, senón tamén os seus compoñentes e marcas de servizo (e ás veces non se corresponde nada co número, quizais polo momento, pero para nós isto non é tan importante), polo que é máis correcto falar sempre especificamente do número de números en si, e non de símbolos. Non obstante, a continuación, por motivos de brevidade, vou usar a miúdo a palabra "símbolo", implicando o termo "punto de código".

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Planos Unicode. Como podes ver, a maior parte del (avións 4 a 13) aínda está sen usar.

O máis notable é que toda a "polpa" principal está no plano cero, chámase "Plano multilingüe básico". Se unha liña contén texto nunha das linguas modernas (incluído o chinés), non irás máis aló deste plano. Pero tampouco podes cortar o resto de Unicode; por exemplo, os emojis sitúanse principalmente ao final de o seguinte avión",Plano Complementario Multilingüe"(esténdese desde 0x10000 para 0x1FFFF). Entón, UTF-16 fai isto: todos os caracteres que se atopan dentro Plano multilingüe básico, están codificados "tal cual" cun número de dous bytes correspondente. Non obstante, algúns dos números deste intervalo non indican caracteres específicos en absoluto, pero indican que despois deste par de bytes debemos considerar outro: ao combinar os valores destes catro bytes, obtemos un número que abarca todo o intervalo Unicode válido. Esta idea chámase "parellas subrogadas"; quizais xa escoitou falar delas.

Polo tanto, UTF-16 require dous ou (en casos moi raros) catro bytes por "punto de código". Isto é mellor que usar catro bytes todo o tempo, pero o latín (e outros caracteres ASCII) cando se codifica deste xeito desperdicia a metade do espazo en ceros. UTF-8 está deseñado para corrixir isto: ASCII nel ocupa, como antes, só un byte; códigos de 0x80 para 0x7FF - dous bytes; dende 0x800 para 0xFFFF - tres, e dende 0x10000 para 0x10FFFF - catro. Por unha banda, o alfabeto latino fíxose bo: volveu a compatibilidade con ASCII e a distribución "espállase" máis uniformemente de 1 a 4 bytes. Pero os alfabetos distintos do latín, por desgraza, non se benefician de ningún xeito en comparación co UTF-16, e moitos agora requiren tres bytes en lugar de dous; o rango cuberto por un rexistro de dous bytes reduciuse 32 veces, con 0xFFFF para 0x7FF, e nin o chinés nin, por exemplo, o xeorxiano están incluídos nel. Cirílico e outros cinco alfabetos - hurray - lucky, 2 bytes por carácter.

Por que ocorre isto? Vexamos como UTF-8 representa os códigos de caracteres:
Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Directamente para representar números, úsanse aquí os bits marcados co símbolo x. Pódese ver que nun rexistro de dous bytes só hai 11 bits deste tipo (sobre 16). Os bits principais aquí só teñen unha función auxiliar. No caso dun rexistro de catro bytes, 21 dos 32 bits son asignados para o número de punto de código; parece que tres bytes (que dan un total de 24 bits) serían suficientes, pero os marcadores de servizo consumen demasiado.

Isto é malo? En realidade non. Por unha banda, se nos preocupa moito o espazo, temos algoritmos de compresión que poden eliminar facilmente toda a entropía e a redundancia extra. Por outra banda, o obxectivo de Unicode era proporcionar a codificación máis universal posible. Por exemplo, podemos confiar unha liña codificada en UTF-8 a un código que antes funcionaba só con ASCII, e non ter medo de que vexa un carácter do intervalo ASCII que en realidade non está alí (despois de todo, en UTF-8 todos bytes que comezan polo bit cero; isto é exactamente o que é ASCII). E se de súpeto queremos cortar unha pequena cola dunha cadea grande sen descodificala desde o principio (ou restaurar parte da información despois dunha sección danada), é fácil para nós atopar o desfase onde comeza un personaxe (é suficiente para saltar bytes que teñen un prefixo de bit 10).

Por que entón inventar algo novo?

Ao mesmo tempo, en ocasións hai situacións nas que os algoritmos de compresión como o deflate son pouco aplicables, pero quere conseguir un almacenamento compacto de cadeas. Persoalmente, atopeime con este problema cando pensaba na construción árbore de prefixos comprimidos para un dicionario grande que inclúa palabras en linguas arbitrarias. Por unha banda, cada palabra é moi curta, polo que comprimila será ineficaz. Por outra banda, a implementación da árbore que considerei foi deseñada para que cada byte da cadea almacenada xerase un vértice de árbore separado, polo que minimizar o seu número era moi útil. Na miña biblioteca Az.js (Como en pimorfia 2, no que se basea) un problema semellante pódese resolver de forma sinxela: as cadeas empaquetadas DAWG-dicionario, almacenado alí bo vello CP1251. Pero, como é fácil de entender, isto funciona ben só para un alfabeto limitado: non se pode engadir unha liña en chinés a un dicionario deste tipo.

Por separado, gustaríame sinalar un matiz desagradable máis que xorde ao usar UTF-8 nunha estrutura de datos deste tipo. A imaxe de arriba mostra que cando un carácter se escribe como dous bytes, os bits relacionados co seu número non veñen seguidos, senón que están separados por un par de bits. 10 no medio: 110xxxxx 10xxxxxx. Debido a isto, cando os 6 bits inferiores do segundo byte desbordan no código de caracteres (é dicir, prodúcese unha transición). 1011111110000000), entón o primeiro byte tamén cambia. Resulta que a letra "p" denotase por bytes 0xD0 0xBF, e a seguinte "r" xa está 0xD1 0x80. Nunha árbore de prefixos, isto leva á división do nodo pai en dous: un para o prefixo 0xD0, e outra para 0xD1 (aínda que todo o alfabeto cirílico só podería codificarse polo segundo byte).

Que conseguín

Ante este problema, decidín practicar xogos con bits e, ao mesmo tempo, familiarizarme un pouco mellor coa estrutura de Unicode no seu conxunto. O resultado foi o formato de codificación UTF-C ("C" para compacto), que non gasta máis de 3 bytes por punto de código e moitas veces só permite gastar un byte extra para toda a liña codificada. Isto leva ao feito de que en moitos alfabetos non ASCII esa codificación resulta ser 30-60% máis compacto que UTF-8.

Presentei exemplos de implementación de algoritmos de codificación e decodificación no formulario Bibliotecas JavaScript e Go, podes usalos libremente no teu código. Pero seguirei subliñando que, en certo sentido, este formato segue sendo unha "bicicleta" e non recomendo usalo. sen entender por que o necesitas. Isto aínda é máis un experimento que unha seria "mellora de UTF-8". Non obstante, o código alí está escrito de forma ordenada, concisa, cunha gran cantidade de comentarios e cobertura de probas.

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Resultados da proba e comparación con UTF-8

Eu tamén o fixen páxina de demostración, onde podes avaliar o rendemento do algoritmo e, a continuación, contarei máis sobre os seus principios e proceso de desenvolvemento.

Eliminación de bits redundantes

Tomei UTF-8 como base, por suposto. O primeiro e máis obvio que se pode cambiar nel é reducir o número de bits de servizo en cada byte. Por exemplo, o primeiro byte en UTF-8 sempre comeza con calquera dos dous 0, ou con 11 - un prefixo 10 Só o teñen os seguintes bytes. Substitúamos o prefixo 11 en 1, e para os próximos bytes eliminaremos os prefixos por completo. Que pasará?

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

Agarda, onde está o rexistro de catro bytes? Pero xa non é necesario: ao escribir en tres bytes, agora temos 21 bits dispoñibles e isto é suficiente para todos os números ata 0x10FFFF.

Que sacrificamos aquí? O máis importante é a detección de límites de caracteres desde unha localización arbitraria no búfer. Non podemos apuntar a un byte arbitrario e atopar o comezo do seguinte carácter a partir del. Esta é unha limitación do noso formato, pero na práctica isto é raramente necesario. Normalmente somos capaces de percorrer o búfer desde o principio (especialmente cando se trata de liñas curtas).

A situación coa cobertura de idiomas con 2 bytes tamén foi mellor: agora o formato de dous bytes dá un rango de 14 bits, e estes son códigos ata 0x3FFF. Os chineses teñen mala sorte (os seus personaxes van principalmente entre 0x4E00 para 0x9FFF), pero os xeorxianos e moitos outros pobos divírtense máis: as súas linguas tamén encaixan en 2 bytes por carácter.

Introduza o estado do codificador

Pensemos agora nas propiedades das propias liñas. O dicionario contén con máis frecuencia palabras escritas en caracteres do mesmo alfabeto, e isto tamén é certo para moitos outros textos. Sería bo indicar este alfabeto unha vez e, a continuación, indicar só o número da letra dentro del. A ver se a disposición dos caracteres na táboa Unicode nos axuda.

Como se mencionou anteriormente, Unicode divídese en avión 65536 códigos cada un. Pero esta non é unha división moi útil (como xa se dixo, a maioría das veces estamos no plano cero). Máis interesante é a división por bloques. Estes intervalos xa non teñen unha lonxitude fixa e son máis significativos: por regra xeral, cada un combina caracteres do mesmo alfabeto.

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Un bloque que contén caracteres do alfabeto bengalí. Desafortunadamente, por razóns históricas, este é un exemplo de empaquetado non moi denso: 96 caracteres están espallados caóticamente en 128 puntos de código de bloque.

Os inicios dos bloques e os seus tamaños son sempre múltiplos de 16; isto faise simplemente por comodidade. Ademais, moitos bloques comezan e rematan en valores que son múltiplos de 128 ou incluso 256; por exemplo, o alfabeto cirílico básico ocupa 256 bytes de 0x0400 para 0x04FF. Isto é bastante cómodo: se gardamos o prefixo unha vez 0x04, entón calquera carácter cirílico pódese escribir nun byte. É certo, deste xeito perderemos a oportunidade de volver ao ASCII (e a calquera outro personaxe en xeral). Polo tanto facemos isto:

  1. Dous bytes 10yyyyyy yxxxxxxx non só denotan un símbolo cun número yyyyyy yxxxxxxx, pero tamén cambiar alfabeto actual en yyyyyy y0000000 (é dicir, lembramos todos os bits excepto os menos significativos 7 bit);
  2. Un byte 0xxxxxxx este é o carácter do alfabeto actual. Só hai que engadirlle ao offset que lembramos no paso 1. Aínda que non cambiamos o alfabeto, o offset é cero, polo que mantivemos a compatibilidade con ASCII.

Igualmente para códigos que requiren 3 bytes:

  1. Tres bytes 110yyyyy yxxxxxxx xxxxxxxx indica un símbolo cun número yyyyyy yxxxxxxx xxxxxxxx, cambio alfabeto actual en yyyyyy y0000000 00000000 (Lembrouse de todo menos dos máis novos 15 bit), e marque a caixa na que estamos agora longo modo (ao cambiar o alfabeto de novo a un de dobre byte, restableceremos esta bandeira);
  2. Dous bytes 0xxxxxxx xxxxxxxx no modo longo é o carácter do alfabeto actual. Do mesmo xeito, engadímolo co desplazamento do paso 1. A única diferenza é que agora lemos dous bytes (porque cambiamos a este modo).

Parece ben: agora aínda que necesitamos codificar caracteres do mesmo intervalo Unicode de 7 bits, gastamos 1 byte extra ao principio e un total de un byte por carácter.

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Traballando desde unha das versións anteriores. Xa a miúdo supera a UTF-8, pero aínda hai marxe para mellorar.

Que é peor? En primeiro lugar, temos unha condición, a saber compensación do alfabeto actual e caixa de verificación modo longo. Isto limítanos aínda máis: agora os mesmos caracteres pódense codificar de forma diferente en diferentes contextos. A busca de subcadeas, por exemplo, terá que facerse tendo isto en conta, e non só comparando bytes. En segundo lugar, axiña que cambiamos o alfabeto, volveuse mal coa codificación de caracteres ASCII (e isto non é só o alfabeto latino, senón tamén a puntuación básica, incluídos os espazos) - requiren cambiar o alfabeto de novo a 0, é dicir, de novo un byte extra (e despois outro para volver ao noso punto principal).

Un alfabeto é bo, dous é mellor

Intentemos cambiar un pouco os nosos prefixos de bit, espremendo un máis aos tres descritos anteriormente:

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

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8

Agora, nun rexistro de dous bytes hai un bit menos dispoñible: puntos de código ata 0x1FFFpero non 0x3FFF. Non obstante, aínda é notablemente maior que nos códigos UTF-8 de dobre byte, a maioría das linguas comúns aínda encaixan, a perda máis notable caeu. hiragana и katakana, os xaponeses están tristes.

Cal é este novo código? 11xxxxxx? Este é un pequeno "alixo" de 64 caracteres de tamaño, que complementa o noso alfabeto principal, polo que o chamei auxiliar (auxiliar) alfabeto. Cando cambiamos o alfabeto actual, unha peza do alfabeto antigo faise auxiliar. Por exemplo, cambiamos de ASCII ao cirílico: o alixo contén agora 64 caracteres que contén Alfabeto latino, números, espazo e coma (insercións máis frecuentes en textos non ASCII). Volve ao ASCII e a parte principal do alfabeto cirílico converterase no alfabeto auxiliar.

Grazas ao acceso a dous alfabetos, podemos manexar un gran número de textos cun custo mínimo para cambiar de alfabeto (a maioría das veces a puntuación levará a un retorno ao ASCII, pero despois obteremos moitos caracteres non ASCII do alfabeto adicional, sen cambiando de novo).

Bonificación: prefixo do subalfabeto 11xxxxxx e escollendo a súa compensación inicial para ser 0xC0, obtemos compatibilidade parcial con CP1252. Noutras palabras, moitos (pero non todos) textos de Europa occidental codificados en CP1252 terán o mesmo aspecto en UTF-C.

Aquí, porén, xorde unha dificultade: como obter un auxiliar do alfabeto principal? Podes deixar a mesma compensación, pero -por desgraza- aquí a estrutura Unicode xa xoga contra nós. Moitas veces a parte principal do alfabeto non está ao principio do bloque (por exemplo, a capital rusa "A" ten o código 0x0410, aínda que o bloque cirílico comeza por 0x0400). Así, tendo os primeiros 64 caracteres no alixo, é posible que perdamos o acceso á parte final do alfabeto.

Para solucionar este problema, pasei manualmente por algúns bloques correspondentes a diferentes idiomas, e especifiquei a compensación do alfabeto auxiliar dentro do principal para eles. O alfabeto latino, como excepción, foi xeralmente reordenado como base64.

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8

Últimos retoques

Por fin pensemos onde podemos mellorar algo.

Teña en conta que o formato 101xxxxx xxxxxxxx xxxxxxxx permítelle codificar números ata 0x1FFFFF, e Unicode remata antes, ás 0x10FFFF. Noutras palabras, o último punto de código representarase como 10110000 11111111 11111111. Polo tanto, podemos dicir que se o primeiro byte é da forma 1011xxxx (onde xxxx maior que 0), entón significa outra cousa. Por exemplo, pode engadir alí outros 15 caracteres que están constantemente dispoñibles para codificar nun byte, pero decidín facelo doutro xeito.

Vexamos aqueles bloques Unicode que precisan agora tres bytes. Basicamente, como xa se mencionou, son caracteres chineses, pero é difícil facer nada con eles, hai 21 mil deles. Pero alí tamén voaron hiragana e katakana, e xa non hai tantos, menos de douscentos. E, xa que lembramos os xaponeses, tamén hai emojis (de feito, están espallados por moitos lugares en Unicode, pero os bloques principais están no rango 0x1F300 - 0x1FBFF). Se pensas no feito de que agora hai emojis que se ensamblan desde varios puntos de código á vez (por exemplo, o emoji ‍‍‍Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8 consta de ata 7 códigos!), entón é unha vergoña gastar tres bytes en cada un (7×3 = 21 bytes por mor dunha icona, un pesadelo).

Polo tanto, seleccionamos algúns intervalos seleccionados correspondentes a emoji, hiragana e katakana, renumeralos nunha lista continua e codificamos como dous bytes en lugar de tres:

1011xxxx xxxxxxxx

Genial: o emoji mencionado anteriormenteOutra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8, que consta de 7 puntos de código, leva 8 bytes en UTF-25 e encaixámolo 14 (exactamente dous bytes para cada punto de código). Por certo, Habr negouse a dixerilo (tanto no editor antigo como no novo), polo que tiven que inserilo cunha imaxe.

Imos tentar solucionar un problema máis. Como lembramos, o alfabeto básico é esencialmente alto 6 bits, que temos en conta e pegamos ao código de cada seguinte símbolo descodificado. No caso dos caracteres chineses que están no bloque 0x4E00 - 0x9FFF, este é o bit 0 ou 1. Isto non é moi cómodo: necesitaremos cambiar constantemente o alfabeto entre estes dous valores (é dicir, gastar tres bytes). Pero teña en conta que no modo longo, do propio código podemos restar o número de caracteres que codificamos usando o modo curto (despois de todos os trucos descritos anteriormente, este é 10240) - entón o rango de xeroglíficos cambiará a 0x2600 - 0x77FF, e neste caso, en todo este intervalo, os 6 bits máis significativos (de 21) serán iguais a 0. Así, as secuencias de xeroglíficos utilizarán dous bytes por xeroglífico (o que é óptimo para un intervalo tan grande), sen provocando cambios de alfabeto.

Solucións alternativas: SCSU, BOCU-1

Os expertos en Unicode, despois de ler o título do artigo, probablemente se apresuren a lembrarche que directamente entre os estándares Unicode hai Esquema de compresión estándar para Unicode (SCSU), que describe un método de codificación moi similar ao descrito no artigo.

Admítoo sinceramente: souben da súa existencia só despois de estar profundamente inmerso na escritura da miña decisión. Se o soubera desde o principio, probablemente tería tentado escribir unha implementación en lugar de elaborar o meu propio enfoque.

O interesante é que SCSU usa ideas moi semellantes ás que se me ocorreron eu só (no canto do concepto de “alfabetos” usan “ventanas”, e hai máis delas dispoñibles das que teño). Ao mesmo tempo, este formato tamén ten inconvenientes: achégase un pouco máis aos algoritmos de compresión que aos de codificación. En particular, o estándar ofrece moitos métodos de representación, pero non di como elixir o óptimo; para iso, o codificador debe usar algún tipo de heurística. Así, un codificador SCSU que produza un bo empaquetado será máis complexo e máis engorroso que o meu algoritmo.

Para comparar, transferín unha implementación relativamente sinxela de SCSU a JavaScript: en termos de volume de código resultou ser comparable ao meu UTF-C, pero nalgúns casos o resultado foi decenas de por cento peor (ás veces pode superalo, pero non por moito). Por exemplo, os textos en hebreo e grego foron codificados por UTF-C 60% mellor que SCSU (probablemente polos seus alfabetos compactos).

Por separado, engadirei que ademais de SCSU tamén hai outra forma de representar Unicode de forma compacta: BOCU-1, pero ten como obxectivo a compatibilidade MIME (que non precisaba) e adopta un enfoque lixeiramente diferente para a codificación. Non avalei a súa eficacia, pero paréceme que é pouco probable que sexa superior á SCSU.

Posibles melloras

O algoritmo que presentei non é universal por deseño (probablemente é aquí onde os meus obxectivos diverxen máis dos obxectivos do Consorcio Unicode). Xa mencionei que foi desenvolvido principalmente para unha tarefa (almacenar un dicionario multilingüe nunha árbore de prefixos), e algunhas das súas características poden non ser moi adecuadas para outras tarefas. Pero o feito de que non sexa un estándar pode ser unha vantaxe - pode modificalo facilmente para adaptalo ás súas necesidades.

Por exemplo, do xeito obvio podes desfacerte da presenza de estado, facer codificación sen estado; simplemente non actualices variables offs, auxOffs и is21Bit no codificador e descodificador. Neste caso, non será posible empaquetar de forma efectiva secuencias de caracteres do mesmo alfabeto, pero haberá unha garantía de que o mesmo carácter estea sempre codificado cos mesmos bytes, independentemente do contexto.

Ademais, pode adaptar o codificador a un idioma específico cambiando o estado predeterminado; por exemplo, centrándose nos textos en ruso, configurar o codificador e o descodificador ao principio. offs = 0x0400 и auxOffs = 0. Isto ten sentido especialmente no caso do modo sen estado. En xeral, isto será semellante ao uso da antiga codificación de oito bits, pero sen eliminar a posibilidade de inserir caracteres de todo Unicode segundo sexa necesario.

Outro inconveniente mencionado anteriormente é que no texto grande codificado en UTF-C non hai un xeito rápido de atopar o límite de caracteres máis próximo a un byte arbitrario. Se cortas os últimos, digamos, 100 bytes do búfer codificado, corres o risco de obter lixo co que non podes facer nada. A codificación non está deseñada para almacenar rexistros de varios gigabytes, pero en xeral pódese corrixir. Byte 0xBF nunca debe aparecer como primeiro byte (pero pode ser o segundo ou terceiro). Polo tanto, ao codificar, pode inserir a secuencia 0xBF 0xBF 0xBF cada, digamos, 10 KB; entón, se precisa atopar un límite, será suficiente para escanear a peza seleccionada ata que se atope un marcador similar. Seguindo o último 0xBF está garantido que é o comezo dun personaxe. (Ao decodificar, esta secuencia de tres bytes, por suposto, terá que ser ignorada).

Resumo

Se leches ata aquí, parabéns! Espero que, coma min, aprendiches algo novo (ou refrescou a memoria) sobre a estrutura de Unicode.

Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8
Páxina de demostración. O exemplo do hebreo mostra as vantaxes sobre UTF-8 e SCSU.

A investigación descrita anteriormente non debe considerarse unha violación dos estándares. Non obstante, en xeral estou satisfeito cos resultados do meu traballo, polo que estou feliz con eles compartir: por exemplo, unha biblioteca JS reducida só pesa 1710 bytes (e non ten dependencias, por suposto). Como mencionei anteriormente, o seu traballo pódese atopar en páxina de demostración (tamén hai un conxunto de textos nos que se pode comparar con UTF-8 e SCSU).

Finalmente, chamarei de novo a atención sobre os casos nos que se usa UTF-C non paga a pena:

  • Se as liñas son suficientemente longas (de 100 a 200 caracteres). Neste caso, deberías pensar en usar algoritmos de compresión como deflate.
  • Se necesitas Transparencia ASCII, é dicir, é importante para ti que as secuencias codificadas non conteñan códigos ASCII que non estaban na cadea orixinal. Pódese evitar a necesidade diso se, ao interactuar con API de terceiros (por exemplo, traballar cunha base de datos), pasa o resultado da codificación como un conxunto abstracto de bytes e non como cadeas. Se non, corre o risco de sufrir vulnerabilidades inesperadas.
  • Se queres poder atopar rapidamente os límites de caracteres nun desfase arbitrario (por exemplo, cando parte dunha liña está danada). Isto pódese facer, pero só escaneando a liña desde o principio (ou aplicando a modificación descrita no apartado anterior).
  • Se precisa realizar rapidamente operacións sobre o contido das cadeas (clasificalas, busca subcadeas nelas, concatenalas). Isto require que as cadeas se descodifiquen primeiro, polo que UTF-C será máis lento que UTF-8 nestes casos (pero máis rápido que os algoritmos de compresión). Dado que a mesma cadea sempre se codifica da mesma forma, non é necesaria a comparación exacta da decodificación e pódese facer byte por byte.

Actualización: usuario Tyomitch nos comentarios a continuación publicou un gráfico que destaca os límites de aplicabilidade de UTF-C. Mostra que UTF-C é máis eficiente que un algoritmo de compresión de propósito xeral (unha variación de LZW) sempre que a cadea empaquetada sexa máis curta ~140 caracteres (non obstante, observo que a comparación realizouse nun texto; para outras linguas o resultado pode diferir).
Outra bicicleta: almacenamos cadeas Unicode un 30-60 % máis compactas que UTF-8

Fonte: www.habr.com

Engadir un comentario