Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8

Se sei uno sviluppatore e devi affrontare il compito di scegliere una codifica, Unicode sarà quasi sempre la soluzione giusta. Il metodo di rappresentazione specifico dipende dal contesto, ma molto spesso anche qui esiste una risposta universale: UTF-8. La cosa buona è che ti permette di utilizzare tutti i caratteri Unicode senza spendere troppo molti byte nella maggior parte dei casi. È vero, per le lingue che utilizzano più del semplice alfabeto latino, almeno “non troppo”. due byte per carattere. Possiamo fare di meglio senza tornare alle codifiche preistoriche che ci limitano a soli 256 caratteri disponibili?

Di seguito propongo di familiarizzare con il mio tentativo di rispondere a questa domanda e di implementare un algoritmo relativamente semplice che consente di memorizzare righe nella maggior parte delle lingue del mondo senza aggiungere la ridondanza presente in UTF-8.

Disclaimer. Faccio subito alcune importanti riserve: la soluzione descritta non è offerta come sostituto universale di UTF-8, è adatto solo in un ristretto elenco di casi (ne parleremo più avanti) e in nessun caso dovrebbe essere utilizzato per interagire con API di terze parti (che non ne sono nemmeno a conoscenza). Molto spesso, gli algoritmi di compressione generici (ad esempio, sgonfiaggio) sono adatti per l'archiviazione compatta di grandi volumi di dati di testo. Inoltre, già nel processo di creazione della mia soluzione, ho trovato uno standard esistente nello stesso Unicode, che risolve lo stesso problema: è un po' più complicato (e spesso peggiore), ma è comunque uno standard accettato, e non solo messo insieme sul ginocchio. Ti parlerò anche di lui.

Informazioni su Unicode e UTF-8

Per cominciare, qualche parola su di cosa si tratta Unicode и UTF-8.

Come sai, le codifiche a 8 bit erano popolari. Con loro tutto era semplice: 256 caratteri possono essere numerati con numeri da 0 a 255, e i numeri da 0 a 255 possono ovviamente essere rappresentati come un byte. Se torniamo all'inizio, la codifica ASCII è completamente limitata a 7 bit, quindi il bit più significativo nella sua rappresentazione in byte è zero, e la maggior parte delle codifiche a 8 bit sono compatibili con esso (differiscono solo nella parte "superiore" parte, dove il bit più significativo è uno ).

In che modo Unicode differisce da tali codifiche e perché sono associate così tante rappresentazioni specifiche: UTF-8, UTF-16 (BE e LE), UTF-32? Risolviamo la cosa in ordine.

Lo standard Unicode di base descrive solo la corrispondenza tra i caratteri (e in alcuni casi, i singoli componenti dei caratteri) e i loro numeri. E ci sono molti numeri possibili in questo standard: da 0x00 a 0x10FFFF (1 pezzi). Se volessimo inserire in una variabile un numero compreso in un intervallo di questo tipo, non ci basterebbero né 114 né 112 byte. E poiché i nostri processori non sono progettati per lavorare con numeri a tre byte, saremmo costretti a utilizzare fino a 1 byte per carattere! Questo è UTF-2, ma è proprio a causa di questo "spreco" che questo formato non è popolare.

Fortunatamente, l'ordine dei caratteri in Unicode non è casuale. Il loro intero set è diviso in 17 "aerei", ognuno dei quali contiene 65536 (0x10000) «punti di codice" Il concetto di "punto di codice" qui è semplice numero del carattere, assegnatogli da Unicode. Ma, come accennato in precedenza, in Unicode non sono numerati solo i singoli caratteri, ma anche i loro componenti e i marchi di servizio (e talvolta al numero non corrisponde proprio nulla - forse per il momento, ma per noi questo non è così importante), quindi è più corretto parlare sempre specificamente del numero dei numeri stessi e non dei simboli. Tuttavia, nel seguito, per brevità, userò spesso la parola “simbolo”, implicando il termine “punto di codice”.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Piani Unicode. Come puoi vedere, la maggior parte (piani dal 4 al 13) è ancora inutilizzata.

Ciò che è più notevole è che tutta la “polpa” principale si trova nel piano zero, si chiama "Aereo multilingue di base". Se una riga contiene testo in una delle lingue moderne (incluso il cinese), non andrai oltre questo piano. Ma non puoi nemmeno tagliare il resto di Unicode - ad esempio, gli emoji si trovano principalmente alla fine di il prossimo aereo,"Piano multilingue supplementare"(si estende da 0x10000 a 0x1FFFF). Quindi UTF-16 fa questo: tutti i caratteri che rientrano all'interno Aereo multilingue di base, sono codificati "così come sono" con un numero di due byte corrispondente. Tuttavia, alcuni numeri in questo intervallo non indicano affatto caratteri specifici, ma indicano che dopo questa coppia di byte dobbiamo considerarne un'altra: combinando insieme i valori di questi quattro byte, otteniamo un numero che copre l'intero intervallo Unicode valido. Questa idea si chiama “coppie surrogate”: potresti averne sentito parlare.

Quindi UTF-16 richiede due o (in casi molto rari) quattro byte per "punto di codice". Questo è meglio che usare sempre quattro byte, ma il latino (e altri caratteri ASCII) quando codificati in questo modo spreca metà dello spazio sugli zeri. UTF-8 è progettato per correggere questo: ASCII occupa, come prima, solo un byte; codici da 0x80 a 0x7FF - due byte; da 0x800 a 0xFFFF - tre e da 0x10000 a 0x10FFFF - quattro. Da un lato, l'alfabeto latino è diventato buono: è tornata la compatibilità con ASCII e la distribuzione è più uniformemente “distribuita” da 1 a 4 byte. Ma gli alfabeti diversi dal latino, ahimè, non beneficiano in alcun modo rispetto a UTF-16, e molti ora richiedono tre byte invece di due: l'intervallo coperto da un record a due byte si è ridotto di 32 volte, con 0xFFFF a 0x7FF, e né il cinese né, ad esempio, il georgiano vi sono inclusi. Cirillico e altri cinque alfabeti - evviva - fortunato, 2 byte per carattere.

Perché succede questo? Vediamo come UTF-8 rappresenta i codici carattere:
Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Per rappresentare direttamente i numeri vengono qui utilizzati i bit contrassegnati con il simbolo x. Si può vedere che in un record di due byte ci sono solo 11 di questi bit (su 16). I bit iniziali qui hanno solo una funzione ausiliaria. In un record di quattro byte, per il numero del punto di codice vengono assegnati 21 bit su 32: sembrerebbe che tre byte (per un totale di 24 bit) siano sufficienti, ma i marcatori di servizio consumano troppo.

E' un male? Non proprio. Da un lato, se ci preoccupiamo molto dello spazio, disponiamo di algoritmi di compressione che possono facilmente eliminare tutta l’entropia e la ridondanza extra. D'altra parte, l'obiettivo di Unicode era fornire la codifica più universale possibile. Ad esempio, possiamo affidare una riga codificata in UTF-8 a un codice che prima funzionava solo con ASCII e non aver paura di vedere un carattere dell'intervallo ASCII che in realtà non c'è (dopo tutto, in UTF-8 tutti byte che iniziano dal bit zero: questo è esattamente ciò che è ASCII). E se all'improvviso vogliamo tagliare una piccola coda da una stringa grande senza decodificarla dall'inizio (o ripristinare parte dell'informazione dopo una sezione danneggiata), è facile per noi trovare l'offset dove inizia un carattere (è sufficiente per saltare i byte che hanno un prefisso bit 10).

Perché allora inventare qualcosa di nuovo?

Allo stesso tempo, ci sono occasionalmente situazioni in cui gli algoritmi di compressione come deflate sono scarsamente applicabili, ma si desidera ottenere un'archiviazione compatta delle stringhe. Personalmente, ho riscontrato questo problema pensando alla costruzione albero dei prefissi compresso per un dizionario di grandi dimensioni che includa parole in lingue arbitrarie. Da un lato, ogni parola è molto breve, quindi comprimerla sarà inefficace. D'altra parte, l'implementazione dell'albero che ho considerato è stata progettata in modo tale che ogni byte della stringa memorizzata generasse un vertice dell'albero separato, quindi minimizzarne il numero è stato molto utile. Nella mia biblioteca Az.js (Come in pimorfia2, su cui si basa) un problema simile può essere risolto semplicemente - impacchettando le stringhe DAWG-dizionario, memorizzato lì in buon vecchio CP1251. Ma, come è facile capire, funziona bene solo per un alfabeto limitato: una riga in cinese non può essere aggiunta a un dizionario del genere.

Separatamente, vorrei notare un'altra sfumatura spiacevole che si presenta quando si utilizza UTF-8 in una tale struttura di dati. L'immagine sopra mostra che quando un carattere è scritto come due byte, i bit relativi al suo numero non sono disposti in fila, ma sono separati da una coppia di bit 10 nel mezzo: 110xxxxx 10xxxxxx. Per questo motivo, quando i 6 bit inferiori del secondo byte vanno in overflow nel codice carattere (vale a dire, si verifica una transizione 1011111110000000), allora cambia anche il primo byte. Si scopre che la lettera "p" è denotata da byte 0xD0 0xBF, e la successiva "r" è già 0xD1 0x80. In un albero di prefissi, ciò porta alla divisione del nodo genitore in due: uno per il prefisso 0xD0e un altro per 0xD1 (sebbene l'intero alfabeto cirillico possa essere codificato solo dal secondo byte).

Cosa ho ottenuto

Di fronte a questo problema, ho deciso di esercitarmi con i giochi con i bit e allo stesso tempo di conoscere un po' meglio la struttura di Unicode nel suo complesso. Il risultato è stato il formato di codifica UTF-C ("C" per compatto), che non spende più di 3 byte per punto di codice e molto spesso consente di spendere solo un byte in più per l'intera riga codificata. Ciò porta al fatto che su molti alfabeti non ASCII tale codifica risulta essere 30-60% più compatto di UTF-8.

Ho presentato esempi di implementazione di algoritmi di codifica e decodifica nel modulo Librerie JavaScript e Go, puoi utilizzarli liberamente nel tuo codice. Ma sottolineo comunque che in un certo senso questo formato rimane una “bicicletta” e ne sconsiglio l'utilizzo senza capire perché ne hai bisogno. Questo è ancora più un esperimento che un serio “miglioramento di UTF-8”. Tuttavia, il codice è scritto in modo chiaro, conciso, con un gran numero di commenti e copertura dei test.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Risultati dei test e confronto con UTF-8

L'ho fatto anch'io pagina dimostrativa, dove potrai valutare le prestazioni dell'algoritmo, e poi ti dirò di più sui suoi principi e sul processo di sviluppo.

Eliminazione dei bit ridondanti

Ovviamente ho preso UTF-8 come base. La prima e più ovvia cosa che può essere modificata è ridurre il numero di bit di servizio in ciascun byte. Ad esempio, il primo byte in UTF-8 inizia sempre con uno dei due 0o con 11 - un prefisso 10 Solo i seguenti byte ce l'hanno. Sostituiamo il prefisso 11 su 1, e per i byte successivi rimuoveremo completamente i prefissi. Cosa accadrà?

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

Aspetta, dov'è il record di quattro byte? Ma non serve più: scrivendo in tre byte ora abbiamo a disposizione 21 bit e questo è sufficiente per tutti i numeri fino a 0x10FFFF.

Cosa abbiamo sacrificato qui? La cosa più importante è il rilevamento dei limiti dei caratteri da una posizione arbitraria nel buffer. Non possiamo puntare a un byte arbitrario e trovare da esso l'inizio del carattere successivo. Questa è una limitazione del nostro formato, ma in pratica raramente è necessaria. Di solito siamo in grado di scorrere il buffer fin dall'inizio (soprattutto quando si tratta di righe brevi).

Anche la situazione con la copertura delle lingue con 2 byte è migliorata: ora il formato a due byte fornisce un intervallo di 14 bit, e questi sono codici fino a 0x3FFF. I cinesi sono sfortunati (i loro caratteri vanno per lo più da 0x4E00 a 0x9FFF), ma i georgiani e molti altri popoli si divertono di più: anche le loro lingue rientrano in 2 byte per carattere.

Immettere lo stato dell'encoder

Pensiamo ora alle proprietà delle linee stesse. Il dizionario contiene molto spesso parole scritte in caratteri dello stesso alfabeto, e questo vale anche per molti altri testi. Sarebbe bene indicare questo alfabeto una volta, e poi indicare solo il numero della lettera al suo interno. Vediamo se la disposizione dei caratteri nella tabella Unicode ci aiuterà.

Come accennato in precedenza, Unicode è suddiviso in aereo 65536 codici ciascuno. Ma questa non è una divisione molto utile (come già detto, il più delle volte siamo nel piano zero). Più interessante è la divisione per blocchi. Questi intervalli non hanno più una lunghezza fissa e sono più significativi: di norma ciascuno combina caratteri dello stesso alfabeto.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Un blocco contenente caratteri dell'alfabeto bengalese. Sfortunatamente, per ragioni storiche, questo è un esempio di packaging non molto denso: 96 caratteri sono sparsi in modo caotico su 128 punti di codice a blocchi.

L'inizio dei blocchi e le loro dimensioni sono sempre multipli di 16: questo viene fatto semplicemente per comodità. Inoltre, molti blocchi iniziano e finiscono con valori multipli di 128 o addirittura 256: ad esempio, l'alfabeto cirillico di base occupa 256 byte da 0x0400 a 0x04FF. Questo è abbastanza conveniente: se salviamo il prefisso una volta 0x04, allora qualsiasi carattere cirillico può essere scritto in un byte. È vero, in questo modo perderemo l'opportunità di tornare in ASCII (e in generale in qualsiasi altro carattere). Pertanto facciamo questo:

  1. Due byte 10yyyyyy yxxxxxxx non solo denotano un simbolo con un numero yyyyyy yxxxxxxx, ma anche cambiare alfabeto attuale su yyyyyy y0000000 (cioè ricordiamo tutti i bit tranne quelli meno significativi 7 bit);
  2. Un byte 0xxxxxxx questo è il carattere dell'alfabeto attuale. Deve solo essere aggiunto all'offset che abbiamo ricordato nel passaggio 1. Anche se non abbiamo modificato l'alfabeto, l'offset è zero, quindi abbiamo mantenuto la compatibilità con ASCII.

Allo stesso modo per i codici che richiedono 3 byte:

  1. Tre byte 110yyyyy yxxxxxxx xxxxxxxx indicare un simbolo con un numero yyyyyy yxxxxxxx xxxxxxxx, modifica alfabeto attuale su yyyyyy y0000000 00000000 (ricordava tutto tranne i più giovani 15 bit) e seleziona la casella in cui ci troviamo ora lungo modalità (quando si cambia l'alfabeto in uno a doppio byte, ripristineremo questo flag);
  2. Due byte 0xxxxxxx xxxxxxxx in modalità lunga è il carattere dell'alfabeto corrente. Allo stesso modo, lo aggiungiamo con l'offset del passaggio 1. L'unica differenza è che ora leggiamo due byte (perché siamo passati a questa modalità).

Suona bene: ora, mentre dobbiamo codificare caratteri dello stesso intervallo Unicode a 7 bit, spendiamo 1 byte in più all'inizio e un totale di un byte per carattere.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Funzionando da una delle versioni precedenti. Spesso batte già UTF-8, ma c'è ancora spazio per miglioramenti.

Cosa è peggio? Innanzitutto, abbiamo una condizione, vale a dire offset alfabetico corrente e casella di controllo modalità lunga. Questo ci limita ulteriormente: ora gli stessi caratteri possono essere codificati diversamente in contesti diversi. La ricerca delle sottostringhe, ad esempio, dovrà essere effettuata tenendo conto di questo e non solo confrontando i byte. In secondo luogo, non appena abbiamo cambiato l'alfabeto, è diventato pessimo con la codifica dei caratteri ASCII (e questo non è solo l'alfabeto latino, ma anche la punteggiatura di base, compresi gli spazi) - richiedono di cambiare nuovamente l'alfabeto in 0, cioè ancora un byte in più (e poi un altro per tornare al punto principale).

Un alfabeto è buono, due è meglio

Proviamo a cambiare un po' i nostri prefissi bit, inserendone uno in più oltre ai tre sopra descritti:

0xxxxxxx — 1 byte in modalità normale, 2 in modalità lunga
11xxxxxx — 1 byte
100xxxxx xxxxxxxx - 2 byte
101xxxxx xxxxxxxx xxxxxxxx - 3 byte

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8

Ora in un record da due byte c'è un bit disponibile in meno: i punti di codice fino a 0x1FFFE non 0x3FFF. Tuttavia, è ancora notevolmente più grande rispetto ai codici UTF-8 a doppio byte, la maggior parte delle lingue comuni si adatta ancora, la perdita più evidente è caduta hiragana и katakana, i giapponesi sono tristi.

Cos'è questo nuovo codice? 11xxxxxx? Questa è una piccola "scorta" di 64 caratteri, complementa il nostro alfabeto principale, quindi l'ho chiamato ausiliario (ausiliaria) alfabeto. Quando cambiamo l'alfabeto attuale, un pezzo del vecchio alfabeto diventa ausiliario. Ad esempio, siamo passati da ASCII a cirillico: la scorta ora contiene 64 caratteri contenenti Alfabeto latino, numeri, spazio e virgola (inserimenti più frequenti in testi non ASCII). Torna ad ASCII e la parte principale dell'alfabeto cirillico diventerà l'alfabeto ausiliario.

Grazie all'accesso a due alfabeti, possiamo gestire un gran numero di testi con costi minimi per cambiare alfabeto (la punteggiatura porterà molto spesso al ritorno all'ASCII, ma dopo otterremo molti caratteri non ASCII dall'alfabeto aggiuntivo, senza cambiando di nuovo).

Bonus: anteponendo il sottoalfabeto 11xxxxxx e scegliendo il suo offset iniziale 0xC0, otteniamo una compatibilità parziale con CP1252. In altre parole, molti (ma non tutti) i testi dell'Europa occidentale codificati in CP1252 avranno lo stesso aspetto in UTF-C.

Qui però sorge una difficoltà: come ottenerne uno ausiliario dall'alfabeto principale? Puoi lasciare lo stesso offset, ma - ahimè - qui la struttura Unicode gioca già contro di noi. Molto spesso la parte principale dell'alfabeto non si trova all'inizio del blocco (ad esempio, la capitale russa “A” ha il codice 0x0410, sebbene il blocco cirillico inizi con 0x0400). Pertanto, avendo messo nella scorta i primi 64 caratteri, potremmo perdere l'accesso alla parte finale dell'alfabeto.

Per risolvere questo problema, ho esaminato manualmente alcuni blocchi corrispondenti a diverse lingue e ho specificato per essi l'offset dell'alfabeto ausiliario all'interno di quello principale. L'alfabeto latino, eccezionalmente, veniva generalmente riordinato come base64.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8

Tocchi finali

Pensiamo infine a dove altro possiamo migliorare qualcosa.

Tieni presente che il formato 101xxxxx xxxxxxxx xxxxxxxx consente di codificare numeri fino a 0x1FFFFF, e Unicode termina prima, a 0x10FFFF. In altre parole, l'ultimo punto di codice verrà rappresentato come 10110000 11111111 11111111. Pertanto, possiamo dire che se il primo byte è nella forma 1011xxxx (dove xxxx maggiore di 0), allora significa qualcos'altro. Ad esempio, puoi aggiungere lì altri 15 caratteri che sono costantemente disponibili per la codifica in un byte, ma ho deciso di farlo diversamente.

Diamo ora un'occhiata a quei blocchi Unicode che richiedono tre byte. Fondamentalmente, come già accennato, questi sono caratteri cinesi, ma è difficile farci qualcosa, ce ne sono 21mila. Ma anche hiragana e katakana sono volati lì - e non ce ne sono più così tanti, meno di duecento. E, visto che ci siamo ricordati dei giapponesi, ci sono anche gli emoji (in effetti, sono sparsi in molti posti in Unicode, ma i blocchi principali sono nell'intervallo 0x1F300 - 0x1FBFF). Se pensi al fatto che ora ci sono emoji che sono assemblati da più punti di codice contemporaneamente (ad esempio, l'emoji ‍‍‍Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8 è composto da ben 7 codici!), allora diventa un vero peccato spendere tre byte su ciascuno (7×3 = 21 byte per il bene di un'icona, un incubo).

Pertanto, selezioniamo alcuni intervalli selezionati corrispondenti a emoji, hiragana e katakana, li rinumeriamo in un elenco continuo e li codifichiamo come due byte anziché tre:

1011xxxx xxxxxxxx

Ottimo: la già citata emoji ‍‍‍Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8, composto da 7 punti di codice, occupa 8 byte in UTF-25 e li inseriamo 14 (esattamente due byte per ciascun punto di codice). A proposito, Habr si è rifiutato di digerirlo (sia nel vecchio che nel nuovo editor), quindi ho dovuto inserirlo con un'immagine.

Proviamo a risolvere un altro problema. Come ricordiamo, l'alfabeto di base è essenzialmente 6 bit alti, che teniamo a mente e incolliamo al codice di ogni successivo simbolo decodificato. Nel caso dei caratteri cinesi presenti nel blocco 0x4E00 - 0x9FFF, questo è il bit 0 o 1. Questo non è molto conveniente: dovremo cambiare costantemente l'alfabeto tra questi due valori (cioè spendere tre byte). Ma tieni presente che nella modalità lunga, dal codice stesso possiamo sottrarre il numero di caratteri che codifichiamo utilizzando la modalità breve (dopo tutti i trucchi descritti sopra, questo è 10240) - quindi l'intervallo di geroglifici passerà a 0x2600 - 0x77FF, e in questo caso, in tutto questo intervallo, i 6 bit più significativi (su 21) saranno uguali a 0. Pertanto, le sequenze di geroglifici utilizzeranno due byte per geroglifico (che è ottimale per un intervallo così ampio), senza causando cambi di alfabeto.

Soluzioni alternative: SCSU, BOCU-1

Gli esperti di Unicode, avendo appena letto il titolo dell'articolo, molto probabilmente si affretteranno a ricordarvi che direttamente tra gli standard Unicode c'è Schema di compressione standard per Unicode (SCSU), che descrive un metodo di codifica molto simile a quello descritto nell'articolo.

Lo ammetto onestamente: ho saputo della sua esistenza solo dopo essere stato profondamente immerso nella scrittura della mia decisione. Se lo avessi saputo fin dall'inizio, probabilmente avrei provato a scrivere un'implementazione invece di elaborare il mio approccio.

La cosa interessante è che SCSU utilizza idee molto simili a quelle che mi sono venute in mente da solo (invece del concetto di "alfabeti" usano "finestre" e ce ne sono più disponibili di me). Allo stesso tempo, questo formato presenta anche degli svantaggi: è un po' più vicino agli algoritmi di compressione che a quelli di codifica. In particolare, lo standard fornisce molti metodi di rappresentazione, ma non dice come scegliere quello ottimale: per questo il codificatore deve utilizzare una sorta di euristica. Pertanto, un codificatore SCSU che produca un buon packaging sarà più complesso e ingombrante del mio algoritmo.

Per fare un confronto, ho trasferito un'implementazione relativamente semplice di SCSU in JavaScript: in termini di volume di codice si è rivelato paragonabile al mio UTF-C, ma in alcuni casi il risultato è stato peggiore del decine di% (a volte potrebbe superarlo, ma non di molto). Ad esempio, i testi in ebraico e greco sono stati codificati da UTF-C 60% migliore rispetto a SCSU (probabilmente a causa dei loro alfabeti compatti).

Separatamente, aggiungerò che oltre a SCSU esiste anche un altro modo per rappresentare in modo compatto Unicode: BOCU-1, ma mira alla compatibilità MIME (di cui non avevo bisogno) e adotta un approccio leggermente diverso alla codifica. Non ne ho valutato l'efficacia, ma mi sembra improbabile che sia superiore a SCSU.

Possibili miglioramenti

L'algoritmo che ho presentato non è universale in base alla progettazione (questo è probabilmente il punto in cui i miei obiettivi divergono maggiormente da quelli dell'Unicode Consortium). Ho già detto che è stato sviluppato principalmente per un compito (memorizzare un dizionario multilingue in un albero di prefissi) e alcune delle sue funzionalità potrebbero non essere adatte per altri compiti. Ma il fatto che non sia uno standard può essere un vantaggio - puoi facilmente modificarlo in base alle tue esigenze.

Ad esempio, in modo ovvio puoi eliminare la presenza dello stato, creare una codifica senza stato, semplicemente non aggiornare le variabili offs, auxOffs и is21Bit nel codificatore e decodificatore. In questo caso non sarà possibile confezionare efficacemente sequenze di caratteri dello stesso alfabeto, ma si avrà la garanzia che lo stesso carattere venga sempre codificato con gli stessi byte, indipendentemente dal contesto.

Inoltre, puoi adattare il codificatore a una lingua specifica modificando lo stato predefinito, ad esempio concentrandoti sui testi russi, impostando il codificatore e il decodificatore all'inizio offs = 0x0400 и auxOffs = 0. Ciò ha senso soprattutto nel caso della modalità stateless. In generale, sarà simile all'utilizzo della vecchia codifica a otto bit, ma senza eliminare la possibilità di inserire caratteri da tutto Unicode secondo necessità.

Un altro svantaggio menzionato in precedenza è che nel testo di grandi dimensioni codificato in UTF-C non esiste un modo rapido per trovare il limite del carattere più vicino a un byte arbitrario. Se tagli gli ultimi, diciamo, 100 byte dal buffer codificato, rischi di ottenere spazzatura con cui non puoi fare nulla. La codifica non è progettata per l'archiviazione di registri multi-gigabyte, ma in generale è possibile correggerla. Byte 0xBF non deve mai apparire come primo byte (ma può essere il secondo o il terzo). Pertanto, durante la codifica, è possibile inserire la sequenza 0xBF 0xBF 0xBF ogni, diciamo, 10 KB - quindi, se è necessario trovare un confine, sarà sufficiente scansionare il pezzo selezionato finché non viene trovato un indicatore simile. Dopo l'ultimo 0xBF è garantito che sia l'inizio di un carattere. (Durante la decodifica, questa sequenza di tre byte dovrà, ovviamente, essere ignorata.)

Riassumendo

Se hai letto fin qui, congratulazioni! Spero che tu, come me, abbia imparato qualcosa di nuovo (o rinfrescato la tua memoria) sulla struttura di Unicode.

Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8
Pagina dimostrativa. L'esempio dell'ebraico mostra i vantaggi sia rispetto a UTF-8 che a SCSU.

La ricerca sopra descritta non dovrebbe essere considerata un'invasione degli standard. Tuttavia, sono generalmente soddisfatto dei risultati del mio lavoro, quindi sono contento di loro quota: ad esempio, una libreria JS minimizzata pesa solo 1710 byte (e non ha dipendenze, ovviamente). Come ho detto sopra, il suo lavoro può essere trovato su pagina dimostrativa (esiste anche una serie di testi su cui può essere confrontato con UTF-8 e SCSU).

Infine, attirerò ancora una volta l'attenzione sui casi in cui viene utilizzato UTF-C non ne vale la pena:

  • Se le tue righe sono abbastanza lunghe (da 100 a 200 caratteri). In questo caso, dovresti pensare a utilizzare algoritmi di compressione come deflate.
  • Se avete bisogno Trasparenza ASCII, cioè è importante per te che le sequenze codificate non contengano codici ASCII che non erano nella stringa originale. Questa necessità può essere evitata se, quando si interagisce con API di terze parti (ad esempio, si lavora con un database), si passa il risultato della codifica come un insieme astratto di byte e non come stringhe. Altrimenti, rischi di ottenere vulnerabilità inaspettate.
  • Se vuoi essere in grado di trovare rapidamente i limiti dei caratteri con un offset arbitrario (ad esempio, quando parte di una riga è danneggiata). Questo può essere fatto, ma solo scansionando la linea dall'inizio (o applicando la modifica descritta nella sezione precedente).
  • Se è necessario eseguire rapidamente operazioni sul contenuto delle stringhe (ordinarle, cercare sottostringhe al loro interno, concatenare). Ciò richiede che le stringhe vengano prima decodificate, quindi in questi casi UTF-C sarà più lento di UTF-8 (ma più veloce degli algoritmi di compressione). Poiché la stessa stringa è sempre codificata nello stesso modo, il confronto esatto della decodifica non è richiesto e può essere effettuato byte per byte.

Aggiornare: utente Tyomitch nei commenti qui sotto ha pubblicato un grafico che evidenzia i limiti di applicabilità di UTF-C. Mostra che UTF-C è più efficiente di un algoritmo di compressione generico (una variazione di LZW) purché la stringa compressa sia più corta ~140 caratteri (noto però che il confronto è stato effettuato su un solo testo; per altre lingue il risultato potrebbe differire).
Un'altra bici: conserviamo le stringhe Unicode il 30-60% più compatte rispetto a UTF-8

Fonte: habr.com

Aggiungi un commento