Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8

Se você é um desenvolvedor e se depara com a tarefa de escolher uma codificação, o Unicode quase sempre será a solução certa. O método de representação específico depende do contexto, mas na maioria das vezes há uma resposta universal aqui também - UTF-8. O bom disso é que permite usar todos os caracteres Unicode sem gastar demais muitos bytes na maioria dos casos. É verdade que para línguas que usam mais do que apenas o alfabeto latino, “não muito” é pelo menos dois bytes por caractere. Podemos fazer melhor sem retornar às codificações pré-históricas que nos limitam a apenas 256 caracteres disponíveis?

A seguir proponho que você se familiarize com minha tentativa de responder a essa pergunta e implementar um algoritmo relativamente simples que permite armazenar linhas na maioria dos idiomas do mundo sem adicionar a redundância que existe no UTF-8.

Isenção de responsabilidade. Farei imediatamente algumas ressalvas importantes: a solução descrita não é oferecida como um substituto universal para UTF-8, ele só é adequado em uma lista restrita de casos (mais sobre eles abaixo) e em nenhum caso deve ser usado para interagir com APIs de terceiros (que nem sabem disso). Na maioria das vezes, algoritmos de compactação de uso geral (por exemplo, deflate) são adequados para armazenamento compacto de grandes volumes de dados de texto. Além disso, já no processo de criação da minha solução, encontrei um padrão existente no próprio Unicode, que resolve o mesmo problema - é um pouco mais complicado (e muitas vezes pior), mas ainda assim é um padrão aceito, e não apenas colocado juntos no joelho. Vou te contar sobre ele também.

Sobre Unicode e UTF-8

Para começar, algumas palavras sobre o que é Unicode и UTF-8.

Como você sabe, as codificações de 8 bits costumavam ser populares. Com eles tudo era simples: 256 caracteres podem ser numerados com números de 0 a 255, e números de 0 a 255 podem obviamente ser representados como um byte. Se voltarmos ao início, a codificação ASCII é completamente limitada a 7 bits, então o bit mais significativo em sua representação de bytes é zero, e a maioria das codificações de 8 bits são compatíveis com ela (elas diferem apenas na parte “superior” parte, onde o bit mais significativo é um).

Como o Unicode difere dessas codificações e por que há tantas representações específicas associadas a ele - UTF-8, UTF-16 (BE e LE), UTF-32? Vamos resolver isso em ordem.

O padrão Unicode básico descreve apenas a correspondência entre caracteres (e, em alguns casos, componentes individuais de caracteres) e seus números. E há muitos números possíveis neste padrão - de 0x00 para 0x10FFFF (1 peças). Se quiséssemos colocar um número desse intervalo em uma variável, nem 114 nem 112 bytes seriam suficientes para nós. E como nossos processadores não são projetados para trabalhar com números de três bytes, seríamos forçados a usar até 1 bytes por caractere! Trata-se de UTF-2, mas é justamente por causa desse “desperdício” que esse formato não é popular.

Felizmente, a ordem dos caracteres no Unicode não é aleatória. Todo o seu conjunto é dividido em 17 "aviões", cada um contendo 65536 (0x10000) «pontos de código" O conceito de “ponto de código” aqui é simplesmente número do caractere, atribuído a ele por Unicode. Mas, como mencionado acima, em Unicode não apenas os caracteres individuais são numerados, mas também seus componentes e marcas de serviço (e às vezes nada corresponde ao número - talvez por enquanto, mas para nós isso não é tão importante), então é mais correto sempre falar especificamente sobre o número dos próprios números, e não dos símbolos. No entanto, a seguir, por uma questão de brevidade, usarei frequentemente a palavra “símbolo”, implicando o termo “ponto de código”.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Aviões Unicode. Como você pode ver, a maior parte (planos 4 a 13) ainda não foi utilizada.

O mais notável é que toda a “polpa” principal está no plano zero, é chamada de "Plano multilíngue básico". Se uma linha contiver texto em um dos idiomas modernos (incluindo chinês), você não irá além deste plano. Mas você também não pode cortar o resto do Unicode - por exemplo, os emojis estão localizados principalmente no final de o próximo avião",Plano Multilíngue Suplementar"(estende-se de 0x10000 para 0x1FFFF). Então o UTF-16 faz isso: todos os caracteres dentro Plano multilíngue básico, são codificados “como estão” com um número correspondente de dois bytes. No entanto, alguns dos números neste intervalo não indicam nenhum caractere específico, mas indicam que após este par de bytes precisamos considerar outro - combinando os valores desses quatro bytes, obtemos um número que cobre todo o intervalo Unicode válido. Essa ideia é chamada de “casais substitutos” – você já deve ter ouvido falar deles.

Portanto, o UTF-16 requer dois ou (em casos muito raros) quatro bytes por "ponto de código". Isso é melhor do que usar quatro bytes o tempo todo, mas o latim (e outros caracteres ASCII), quando codificado dessa maneira, desperdiça metade do espaço em zeros. O UTF-8 foi projetado para corrigir isso: o ASCII nele ocupa, como antes, apenas um byte; códigos de 0x80 para 0x7FF - dois bytes; de 0x800 para 0xFFFF - três, e de 0x10000 para 0x10FFFF - quatro. Por um lado, o alfabeto latino tornou-se bom: a compatibilidade com ASCII voltou e a distribuição está “distribuída” de maneira mais uniforme de 1 a 4 bytes. Mas outros alfabetos além do latino, infelizmente, não se beneficiam de forma alguma em comparação com o UTF-16, e muitos agora exigem três bytes em vez de dois - o intervalo coberto por um registro de dois bytes diminuiu 32 vezes, com 0xFFFF para 0x7FF, e nem os chineses nem, por exemplo, os georgianos estão incluídos nele. Cirílico e cinco outros alfabetos - viva - sorte, 2 bytes por caractere.

Por que isso acontece? Vamos ver como o UTF-8 representa os códigos de caracteres:
Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Diretamente para representar números, os bits marcados com o símbolo são usados ​​aqui x. Pode-se ver que em um registro de dois bytes existem apenas 11 desses bits (de 16). Os bits iniciais aqui têm apenas uma função auxiliar. No caso de um registro de quatro bytes, 21 dos 32 bits são alocados para o número do ponto de código - parece que três bytes (que dão um total de 24 bits) seriam suficientes, mas os marcadores de serviço consomem muito.

Isso é ruim? Na verdade. Por um lado, se nos preocupamos muito com o espaço, temos algoritmos de compressão que podem facilmente eliminar toda a entropia e redundância extra. Por outro lado, o objetivo do Unicode era fornecer a codificação mais universal possível. Por exemplo, podemos confiar uma linha codificada em UTF-8 a um código que antes funcionava apenas com ASCII, e não ter medo de ver um caractere do intervalo ASCII que na verdade não existe (afinal, em UTF-8 todos bytes começando com o bit zero - isso é exatamente o que é ASCII). E se de repente quisermos cortar uma pequena cauda de uma string grande sem decodificá-la desde o início (ou restaurar parte da informação após uma seção danificada), é fácil encontrarmos o deslocamento onde um caractere começa (é o suficiente para pular bytes que possuem um prefixo de bit 10).

Por que então inventar algo novo?

Ao mesmo tempo, ocasionalmente há situações em que algoritmos de compactação como deflate são pouco aplicáveis, mas você deseja obter um armazenamento compacto de strings. Pessoalmente, encontrei esse problema ao pensar em construir árvore de prefixo compactada para um grande dicionário incluindo palavras em idiomas arbitrários. Por um lado, cada palavra é muito curta, portanto comprimi-la será ineficaz. Por outro lado, a implementação de árvore que considerei foi projetada para que cada byte da string armazenada gerasse um vértice de árvore separado, portanto, minimizar seu número foi muito útil. Na minha biblioteca Az.js (Como em pimorfia2, no qual se baseia) um problema semelhante pode ser resolvido simplesmente - strings compactadas em DAWG-dicionário, armazenado lá em bom e velho CP1251. Mas, como é fácil de entender, isso funciona bem apenas para um alfabeto limitado - uma linha em chinês não pode ser adicionada a esse dicionário.

Separadamente, gostaria de observar mais uma nuance desagradável que surge ao usar UTF-8 em tal estrutura de dados. A imagem acima mostra que quando um caractere é escrito como dois bytes, os bits relacionados ao seu número não vêm em uma linha, mas são separados por um par de bits 10 No meio: 110xxxxx 10xxxxxx. Por causa disso, quando os 6 bits inferiores do segundo byte estouram no código do caractere (ou seja, ocorre uma transição 1011111110000000), então o primeiro byte também muda. Acontece que a letra “p” é denotada por bytes 0xD0 0xBF, e o próximo “r” já está 0xD1 0x80. Em uma árvore de prefixos, isso leva à divisão do nó pai em dois - um para o prefixo 0xD0, e outro para 0xD1 (embora todo o alfabeto cirílico pudesse ser codificado apenas pelo segundo byte).

O que eu consegui

Diante desse problema, resolvi praticar jogos com bits e ao mesmo tempo conhecer um pouco melhor a estrutura do Unicode como um todo. O resultado foi o formato de codificação UTF-C ("C" para compacto), que não gasta mais do que 3 bytes por ponto de código e muitas vezes permite que você gaste apenas um byte extra para toda a linha codificada. Isto leva ao fato de que em muitos alfabetos não-ASCII tal codificação acaba sendo 30-60% mais compacto que UTF-8.

Apresentei exemplos de implementação de algoritmos de codificação e decodificação na forma Bibliotecas JavaScript e Go, você pode usá-los livremente em seu código. Mas ainda vou enfatizar que de certa forma este formato continua sendo uma “bicicleta” e não recomendo usá-lo sem perceber por que você precisa disso. Isso ainda é mais um experimento do que uma “melhoria séria do UTF-8”. No entanto, o código está escrito de forma organizada e concisa, com um grande número de comentários e cobertura de testes.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Resultados de teste e comparação com UTF-8

eu também fiz página de demonstração, onde você pode avaliar o desempenho do algoritmo, e a seguir contarei mais sobre seus princípios e processo de desenvolvimento.

Eliminando bits redundantes

Tomei como base o UTF-8, é claro. A primeira e mais óbvia coisa que pode ser alterada é reduzir o número de bits de serviço em cada byte. Por exemplo, o primeiro byte em UTF-8 sempre começa com 0, ou com 11 - um prefixo 10 Apenas os seguintes bytes o possuem. Vamos substituir o prefixo 11 em 1, e para os próximos bytes removeremos completamente os prefixos. O que vai acontecer?

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

Espere, onde está o registro de quatro bytes? Mas não é mais necessário - ao escrever em três bytes, temos agora 21 bits disponíveis e isso é suficiente para todos os números até 0x10FFFF.

O que sacrificamos aqui? O mais importante é a detecção dos limites dos caracteres de um local arbitrário no buffer. Não podemos apontar para um byte arbitrário e encontrar nele o início do próximo caractere. Esta é uma limitação do nosso formato, mas na prática raramente é necessário. Geralmente conseguimos percorrer o buffer desde o início (especialmente quando se trata de linhas curtas).

A situação de cobrir idiomas com 2 bytes também melhorou: agora o formato de dois bytes dá um intervalo de 14 bits, e são códigos até 0x3FFF. Os chineses não têm sorte (seus personagens variam principalmente de 0x4E00 para 0x9FFF), mas os georgianos e muitos outros povos se divertem mais - seus idiomas também cabem em 2 bytes por caractere.

Insira o estado do codificador

Vamos agora pensar nas propriedades das próprias linhas. O dicionário geralmente contém palavras escritas em caracteres do mesmo alfabeto, e isso também se aplica a muitos outros textos. Seria bom indicar este alfabeto uma vez e depois indicar apenas o número da letra dentro dele. Vamos ver se a disposição dos caracteres na tabela Unicode nos ajuda.

Como mencionado acima, o Unicode é dividido em o avião 65536 códigos cada. Mas esta não é uma divisão muito útil (como já foi dito, na maioria das vezes estamos no plano zero). Mais interessante é a divisão por blocos. Esses intervalos não têm mais comprimento fixo e são mais significativos - como regra, cada um combina caracteres do mesmo alfabeto.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Um bloco contendo caracteres do alfabeto bengali. Infelizmente, por razões históricas, este é um exemplo de embalagem não muito densa - 96 caracteres estão caoticamente espalhados por 128 pontos de código de bloco.

O início dos blocos e seus tamanhos são sempre múltiplos de 16 - isso é feito simplesmente por conveniência. Além disso, muitos blocos começam e terminam com valores múltiplos de 128 ou mesmo 256 - por exemplo, o alfabeto cirílico básico ocupa 256 bytes de 0x0400 para 0x04FF. Isto é bastante conveniente: se salvarmos o prefixo uma vez 0x04, então qualquer caractere cirílico pode ser escrito em um byte. É verdade que assim perderemos a oportunidade de retornar ao ASCII (e a quaisquer outros caracteres em geral). Portanto fazemos isso:

  1. Dois bytes 10yyyyyy yxxxxxxx não apenas denota um símbolo com um número yyyyyy yxxxxxxx, mas também mudar alfabeto atual em yyyyyy y0000000 (ou seja, lembramos de todos os bits, exceto os menos significativos Bit 7);
  2. Um byte 0xxxxxxx este é o caractere do alfabeto atual. Só precisa ser adicionado ao deslocamento que lembramos no passo 1. Embora não tenhamos alterado o alfabeto, o deslocamento é zero, por isso mantivemos a compatibilidade com ASCII.

Da mesma forma para códigos que requerem 3 bytes:

  1. Três bytes 110yyyyy yxxxxxxx xxxxxxxx indicar um símbolo com um número yyyyyy yxxxxxxx xxxxxxxx, mudar alfabeto atual em yyyyyy y0000000 00000000 (lembrei de tudo menos dos mais novos Bit 15) e marque a caixa em que estamos agora longo modo (ao alterar o alfabeto de volta para um byte duplo, redefiniremos este sinalizador);
  2. Dois bytes 0xxxxxxx xxxxxxxx no modo longo, é o caractere do alfabeto atual. Da mesma forma, adicionamos o deslocamento da etapa 1. A única diferença é que agora lemos dois bytes (porque mudamos para este modo).

Parece bom: agora, embora precisemos codificar caracteres do mesmo intervalo Unicode de 7 bits, gastamos 1 byte extra no início e um total de um byte por caractere.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Trabalhando a partir de uma das versões anteriores. Já supera frequentemente o UTF-8, mas ainda há espaço para melhorias.

O que é pior? Em primeiro lugar, temos uma condição, a saber deslocamento atual do alfabeto e caixa de seleção modo longo. Isto nos limita ainda mais: agora os mesmos caracteres podem ser codificados de forma diferente em contextos diferentes. A busca por substrings, por exemplo, terá que ser feita levando isso em consideração, e não apenas comparando bytes. Em segundo lugar, assim que mudamos o alfabeto, ficou ruim com a codificação dos caracteres ASCII (e este não é apenas o alfabeto latino, mas também a pontuação básica, incluindo espaços) - eles exigem a mudança do alfabeto novamente para 0, ou seja, novamente um byte extra (e depois outro para voltar ao nosso ponto principal).

Um alfabeto é bom, dois é melhor

Vamos tentar mudar um pouco nossos prefixos de bits, acrescentando mais um aos três descritos acima:

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

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8

Agora, em um registro de dois bytes, há um bit disponível a menos - o código aponta até 0x1FFFE não 0x3FFF. No entanto, ainda é visivelmente maior do que nos códigos UTF-8 de byte duplo, as linguagens mais comuns ainda cabem, a perda mais notável ocorreu hiragana и Katakana, os japoneses estão tristes.

Qual é esse novo código? 11xxxxxx? Este é um pequeno “esconderijo” de 64 caracteres, ele complementa nosso alfabeto principal, então chamei de auxiliar (auxiliar) alfabeto. Quando trocamos o alfabeto atual, um pedaço do alfabeto antigo torna-se auxiliar. Por exemplo, mudamos de ASCII para cirílico - o stash agora contém 64 caracteres contendo Alfabeto latino, números, espaço e vírgula (inserções mais frequentes em textos não-ASCII). Volte para ASCII - e a parte principal do alfabeto cirílico se tornará o alfabeto auxiliar.

Graças ao acesso a dois alfabetos, podemos lidar com um grande número de textos com custos mínimos para troca de alfabetos (a pontuação geralmente levará a um retorno ao ASCII, mas depois disso obteremos muitos caracteres não ASCII do alfabeto adicional, sem mudando novamente).

Bônus: prefixando o subalfabeto 11xxxxxx e escolhendo seu deslocamento inicial para ser 0xC0, obtemos compatibilidade parcial com CP1252. Em outras palavras, muitos (mas não todos) textos da Europa Ocidental codificados em CP1252 terão a mesma aparência em UTF-C.

Aqui, porém, surge uma dificuldade: como obter um auxiliar do alfabeto principal? Você pode deixar o mesmo deslocamento, mas - infelizmente - aqui a estrutura Unicode já está jogando contra nós. Muitas vezes, a parte principal do alfabeto não está no início do bloco (por exemplo, a maiúscula russa “A” tem o código 0x0410, embora o bloco cirílico comece com 0x0400). Assim, tendo colocado os primeiros 64 caracteres no stash, podemos perder o acesso à parte final do alfabeto.

Para corrigir esse problema, passei manualmente por alguns blocos correspondentes a diferentes idiomas e especifiquei o deslocamento do alfabeto auxiliar dentro do alfabeto principal para eles. O alfabeto latino, como exceção, foi geralmente reordenado como base64.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8

Toques finais

Vamos finalmente pensar onde mais podemos melhorar alguma coisa.

Observe que o formato 101xxxxx xxxxxxxx xxxxxxxx permite codificar números até 0x1FFFFF, e o Unicode termina mais cedo, em 0x10FFFF. Em outras palavras, o último ponto de código será representado como 10110000 11111111 11111111. Portanto, podemos dizer que se o primeiro byte estiver na forma 1011xxxx (onde xxxx maior que 0), então significa outra coisa. Por exemplo, você pode adicionar mais 15 caracteres que estão constantemente disponíveis para codificação em um byte, mas decidi fazer de forma diferente.

Vejamos os blocos Unicode que requerem três bytes agora. Basicamente, como já mencionado, são caracteres chineses - mas é difícil fazer alguma coisa com eles, são 21 mil. Mas hiragana e katakana também voaram para lá - e não são mais tantos, menos de duzentos. E, já que lembramos dos japoneses, também existem emojis (na verdade, eles estão espalhados em vários lugares em Unicode, mas os blocos principais estão na faixa 0x1F300 - 0x1FBFF). Se você pensar no fato de que agora existem emojis que são montados a partir de vários pontos de código ao mesmo tempo (por exemplo, o emoji ‍‍‍Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8 consiste em até 7 códigos!), então é uma pena gastar três bytes em cada (7×3 = 21 bytes por causa de um ícone, um pesadelo).

Portanto, selecionamos alguns intervalos selecionados correspondentes a emoji, hiragana e katakana, renumeramo-los em uma lista contínua e codificamo-los como dois bytes em vez de três:

1011xxxx xxxxxxxx

Ótimo: o emoji ‍‍‍ acima mencionadoOutra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8, que consiste em 7 pontos de código, ocupa 8 bytes em UTF-25 e os ajustamos em 14 (exatamente dois bytes para cada ponto de código). Aliás, Habr se recusou a digeri-lo (tanto no antigo quanto no novo editor), então tive que inserir uma foto.

Vamos tentar resolver mais um problema. Como lembramos, o alfabeto básico é essencialmente alto 6 bits, que temos em mente e colamos no código de cada próximo símbolo decodificado. No caso de caracteres chineses que estão no bloco 0x4E00 - 0x9FFF, este é o bit 0 ou 1. Isso não é muito conveniente: precisaremos alternar constantemente o alfabeto entre esses dois valores (ou seja, gastar três bytes). Mas observe que no modo longo, podemos subtrair do próprio código o número de caracteres que codificamos usando o modo curto (depois de todos os truques descritos acima, isso é 10240) - então o intervalo de hieróglifos mudará para 0x2600 - 0x77FF, e neste caso, em todo esse intervalo, os 6 bits mais significativos (de 21) serão iguais a 0. Assim, as sequências de hieróglifos usarão dois bytes por hieróglifo (o que é ideal para um intervalo tão grande), sem causando mudanças de alfabeto.

Soluções alternativas: SCSU, BOCU-1

Os especialistas em Unicode, depois de lerem o título do artigo, provavelmente se apressarão em lembrá-lo de que diretamente entre os padrões Unicode existe Esquema de compressão padrão para Unicode (SCSU), que descreve um método de codificação muito semelhante ao descrito no artigo.

Admito honestamente: só soube de sua existência depois de estar profundamente imerso na redação de minha decisão. Se eu soubesse disso desde o início, provavelmente teria tentado escrever uma implementação em vez de criar minha própria abordagem.

O interessante é que o SCSU usa ideias muito semelhantes às que eu mesmo criei (em vez do conceito de “alfabetos” eles usam “janelas”, e há mais delas disponíveis do que eu). Ao mesmo tempo, esse formato também tem desvantagens: está um pouco mais próximo dos algoritmos de compressão do que dos algoritmos de codificação. Em particular, o padrão fornece muitos métodos de representação, mas não diz como escolher o ideal - para isso, o codificador deve usar algum tipo de heurística. Assim, um codificador SCSU que produza um bom empacotamento será mais complexo e mais complicado do que o meu algoritmo.

Para efeito de comparação, transferi uma implementação relativamente simples de SCSU para JavaScript - em termos de volume de código, acabou sendo comparável ao meu UTF-C, mas em alguns casos o resultado foi dezenas de por cento pior (às vezes pode excedê-lo, mas não muito). Por exemplo, textos em hebraico e grego foram codificados por UTF-C 60% melhor que SCSU (provavelmente devido aos seus alfabetos compactos).

Separadamente, acrescentarei que, além do SCSU, há também outra maneira de representar o Unicode de forma compacta - BOCU-1, mas visa compatibilidade MIME (da qual eu não precisava) e adota uma abordagem um pouco diferente para codificação. Não avaliei a sua eficácia, mas parece-me que é improvável que seja superior ao SCSU.

Possíveis melhorias

O algoritmo que apresentei não é universal por design (é provavelmente aqui que meus objetivos divergem mais dos objetivos do Consórcio Unicode). Já mencionei que ele foi desenvolvido principalmente para uma tarefa (armazenar um dicionário multilíngue em uma árvore de prefixos) e alguns de seus recursos podem não ser adequados para outras tarefas. Mas o fato de não ser um padrão pode ser uma vantagem - você pode modificá-lo facilmente para atender às suas necessidades.

Por exemplo, da maneira óbvia, você pode se livrar da presença de estado, fazer codificação sem estado - apenas não atualize variáveis offs, auxOffs и is21Bit no codificador e no decodificador. Neste caso, não será possível empacotar efetivamente sequências de caracteres do mesmo alfabeto, mas haverá a garantia de que o mesmo caractere será sempre codificado com os mesmos bytes, independente do contexto.

Além disso, você pode personalizar o codificador para um idioma específico alterando o estado padrão - por exemplo, concentrando-se em textos russos, defina o codificador e o decodificador no início offs = 0x0400 и auxOffs = 0. Isto faz sentido especialmente no caso do modo sem estado. Em geral, isso será semelhante ao uso da antiga codificação de oito bits, mas sem remover a capacidade de inserir caracteres de todos os Unicode conforme necessário.

Outra desvantagem mencionada anteriormente é que em texto grande codificado em UTF-C não há uma maneira rápida de encontrar o limite de caracteres mais próximo de um byte arbitrário. Se você cortar os últimos, digamos, 100 bytes do buffer codificado, corre o risco de obter lixo com o qual não poderá fazer nada. A codificação não foi projetada para armazenar logs de vários gigabytes, mas em geral isso pode ser corrigido. Byte 0xBF nunca deve aparecer como o primeiro byte (mas pode ser o segundo ou terceiro). Portanto, ao codificar, você pode inserir a sequência 0xBF 0xBF 0xBF cada, digamos, 10 KB - então, se você precisar encontrar um limite, será suficiente escanear a peça selecionada até que um marcador semelhante seja encontrado. Seguindo o último 0xBF é garantido ser o início de um personagem. (Ao decodificar, esta sequência de três bytes precisará, obviamente, ser ignorada.)

Resumindo

Se você leu até aqui, parabéns! Espero que você, assim como eu, tenha aprendido algo novo (ou refrescado sua memória) sobre a estrutura do Unicode.

Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8
Página de demonstração. O exemplo do hebraico mostra as vantagens tanto em relação ao UTF-8 quanto ao SCSU.

A pesquisa acima descrita não deve ser considerada uma violação dos padrões. No entanto, estou geralmente satisfeito com os resultados do meu trabalho, por isso estou feliz com eles partilhar: por exemplo, uma biblioteca JS reduzida pesa apenas 1710 bytes (e não tem dependências, é claro). Como mencionei acima, seu trabalho pode ser encontrado em página de demonstração (há também um conjunto de textos nos quais pode ser comparado com UTF-8 e SCSU).

Por fim, chamarei mais uma vez a atenção para os casos em que o UTF-C é usado não vale a pena:

  • Se suas linhas forem longas o suficiente (de 100 a 200 caracteres). Nesse caso, você deve pensar em usar algoritmos de compressão como deflate.
  • Se você precisar Transparência ASCII, ou seja, é importante para você que as sequências codificadas não contenham códigos ASCII que não estavam na string original. A necessidade disso pode ser evitada se, ao interagir com APIs de terceiros (por exemplo, trabalhar com um banco de dados), você passar o resultado da codificação como um conjunto abstrato de bytes, e não como strings. Caso contrário, você corre o risco de obter vulnerabilidades inesperadas.
  • Se você deseja encontrar rapidamente os limites dos caracteres em um deslocamento arbitrário (por exemplo, quando parte de uma linha está danificada). Isto pode ser feito, mas apenas escaneando a linha desde o início (ou aplicando a modificação descrita na seção anterior).
  • Se você precisar realizar operações rapidamente no conteúdo das strings (classificá-las, procurar substrings nelas, concatenar). Isso exige que as strings sejam decodificadas primeiro, portanto o UTF-C será mais lento que o UTF-8 nesses casos (mas mais rápido que os algoritmos de compactação). Como a mesma string é sempre codificada da mesma maneira, a comparação exata da decodificação não é necessária e pode ser feita byte por byte.

Update: usuário Tyomitch nos comentários abaixo postou um gráfico destacando os limites de aplicabilidade do UTF-C. Mostra que UTF-C é mais eficiente que um algoritmo de compactação de uso geral (uma variação de LZW), desde que a string compactada seja mais curta ~140 caracteres (no entanto, observo que a comparação foi realizada em um texto; para outros idiomas o resultado pode ser diferente).
Outra bicicleta: armazenamos strings Unicode 30-60% mais compactas que UTF-8

Fonte: habr.com

Compre hospedagem confiável para sites com proteção DDoS, servidores VPS VDS 🔥 Compre hospedagem de sites confiável com proteção contra DDoS, servidores VPS/VDS | ProHoster