
Wenn Sie als Entwickler vor der Aufgabe stehen, eine Kodierung auszuwĂ€hlen, ist Unicode fast immer die richtige Lösung. Die konkrete Darstellungsmethode hĂ€ngt vom Kontext ab, aber meistens gibt es auch hier eine universelle Antwort â UTF-8. Das Gute daran ist, dass Sie damit alle Unicode-Zeichen ohne Kosten verwenden können zu viel In den meisten FĂ€llen viele Bytes. Stimmt, fĂŒr Sprachen, die mehr als nur das lateinische Alphabet verwenden, ist ânicht zu vielâ zumindest der Fall zwei Bytes pro Zeichen. Können wir es besser machen, ohne zu prĂ€historischen Kodierungen zurĂŒckzukehren, die uns auf nur 256 verfĂŒgbare Zeichen beschrĂ€nken?
Im Folgenden schlage ich vor, Sie mit meinem Versuch vertraut zu machen, diese Frage zu beantworten und einen relativ einfachen Algorithmus zu implementieren, der es Ihnen ermöglicht, Zeilen in den meisten Sprachen der Welt zu speichern, ohne die Redundanz von UTF-8 hinzuzufĂŒgen.
Haftungsausschluss. Ich werde gleich ein paar wichtige Vorbehalte machen: Die beschriebene Lösung wird nicht als universeller Ersatz fĂŒr UTF-8 angeboten, ist es nur in einer engen Liste von FĂ€llen geeignet (mehr dazu weiter unten) und sollte auf keinen Fall fĂŒr die Interaktion mit APIs von Drittanbietern verwendet werden (die nicht einmal davon wissen). Am hĂ€ufigsten eignen sich allgemeine Komprimierungsalgorithmen (z. B. Deflate) fĂŒr die kompakte Speicherung groĂer Textdatenmengen. DarĂŒber hinaus habe ich bereits bei der Erstellung meiner Lösung einen vorhandenen Standard in Unicode selbst gefunden, der das gleiche Problem löst â er ist etwas komplizierter (und oft schlimmer), aber dennoch ein akzeptierter Standard und nicht einfach ausgedrĂŒckt zusammen auf dem Knie. Ich werde dir auch von ihm erzĂ€hlen.
Ăber Unicode und UTF-8
ZunĂ€chst ein paar Worte darĂŒber, was es ist Unicode Đž UTF-8.
Wie Sie wissen, waren frĂŒher 8-Bit-Kodierungen beliebt. Bei ihnen war alles einfach: 256 Zeichen können mit Zahlen von 0 bis 255 nummeriert werden, und Zahlen von 0 bis 255 lassen sich natĂŒrlich als ein Byte darstellen. Wenn wir zum Anfang zurĂŒckkehren, ist die ASCII-Kodierung vollstĂ€ndig auf 7 Bits beschrĂ€nkt, sodass das höchstwertige Bit in ihrer Byte-Darstellung Null ist und die meisten 8-Bit-Kodierungen damit kompatibel sind (sie unterscheiden sich nur im âoberenâ Bit). Teil, wobei das höchstwertige Bit eins ist).
Wie unterscheidet sich Unicode von diesen Codierungen und warum sind damit so viele spezifische Darstellungen verbunden â UTF-8, UTF-16 (BE und LE), UTF-32? Lasst es uns der Reihe nach klĂ€ren.
Der grundlegende Unicode-Standard beschreibt nur die Entsprechung zwischen Zeichen (und in einigen FĂ€llen einzelnen Zeichenbestandteilen) und ihren Zahlen. Und es gibt viele mögliche Zahlen in diesem Standard â von 0x00 auf 0x10FFFF (1 StĂŒck). Wenn wir eine Zahl in einem solchen Bereich in eine Variable einfĂŒgen wollten, wĂŒrden uns weder 114 noch 112 Bytes ausreichen. Und da unsere Prozessoren nicht besonders fĂŒr die Arbeit mit Drei-Byte-Zahlen ausgelegt sind, wĂ€ren wir gezwungen, bis zu 1 Bytes pro Zeichen zu verwenden! Das ist UTF-2, aber gerade wegen dieser âVerschwendungâ ist dieses Format nicht beliebt.
GlĂŒcklicherweise ist die Reihenfolge der Zeichen in Unicode nicht zufĂ€llig. Ihr gesamtes Set ist in 17" unterteilt.Flugzeuge", die jeweils 65536 (0x10000) "Codepunkte" Das Konzept eines âCodepunktsâ ist hier einfach Zeichennummer, ihm von Unicode zugewiesen. Aber wie oben erwĂ€hnt, werden in Unicode nicht nur einzelne Zeichen nummeriert, sondern auch deren Bestandteile und Dienstleistungszeichen (und manchmal entspricht ĂŒberhaupt nichts der Nummer â vielleicht vorerst, aber fĂŒr uns ist das nicht so wichtig), also Es ist richtiger, immer konkret ĂŒber die Anzahl der Zahlen selbst zu sprechen und nicht ĂŒber Symbole. Der KĂŒrze halber werde ich im Folgenden jedoch hĂ€ufig das Wort âSymbolâ verwenden, was den Begriff âCodepunktâ impliziert.

Unicode-Flugzeuge. Wie Sie sehen, ist der GroĂteil davon (Flugzeuge 4 bis 13) noch ungenutzt.
Das Bemerkenswerteste ist, dass der gesamte Haupt-âZellstoffâ in der Nullebene liegt, man nennt ihn âGrundlegende mehrsprachige Ebene". Wenn eine Zeile Text in einer der modernen Sprachen (einschlieĂlich Chinesisch) enthĂ€lt, kommen Sie nicht ĂŒber diese Ebene hinaus. Sie können aber auch den Rest von Unicode nicht abschneiden â Emojis befinden sich beispielsweise hauptsĂ€chlich am Ende von das nĂ€chste Flugzeugâ,ZusĂ€tzliche mehrsprachige Ebene"(es erstreckt sich von 0x10000 auf 0x1FFFF). UTF-16 macht also Folgendes: alle Zeichen fallen darunter Grundlegende mehrsprachige Ebenewerden âwie sie sindâ mit einer entsprechenden Zwei-Byte-Zahl codiert. Einige der Zahlen in diesem Bereich geben jedoch ĂŒberhaupt keine bestimmten Zeichen an, sondern weisen darauf hin, dass wir nach diesem Bytepaar ein weiteres in Betracht ziehen mĂŒssen â indem wir die Werte dieser vier Bytes miteinander kombinieren, erhalten wir eine abdeckende Zahl den gesamten gĂŒltigen Unicode-Bereich. Diese Idee wird âErsatzpaareâ genannt â Sie haben vielleicht schon davon gehört.
UTF-16 benötigt also zwei oder (in sehr seltenen FĂ€llen) vier Bytes pro âCodepunktâ. Das ist besser als die stĂ€ndige Verwendung von vier Bytes, aber lateinische Zeichen (und andere ASCII-Zeichen) verschwenden bei dieser Codierung die HĂ€lfte des Platzes durch Nullen. UTF-8 soll dies beheben: ASCII belegt darin nach wie vor nur ein Byte; Codes von 0x80 auf 0x7FF - zwei Bytes; aus 0x800 auf 0xFFFF - drei, und von 0x10000 auf 0x10FFFF - vier. Einerseits ist das lateinische Alphabet gut geworden: Die KompatibilitĂ€t mit ASCII ist zurĂŒckgekehrt und die Verteilung ist gleichmĂ€Ăiger von 1 auf 4 Bytes âverteiltâ. Aber andere Alphabete als Latein haben im Vergleich zu UTF-16 leider keinen Vorteil, und viele erfordern jetzt drei statt zwei Bytes â der von einem Zwei-Byte-Datensatz abgedeckte Bereich hat sich mit um das 32-fache verringert 0xFFFF auf 0x7FF, und weder Chinesisch noch beispielsweise Georgisch sind darin enthalten. Kyrillisch und fĂŒnf weitere Alphabete â Hurra â GlĂŒck gehabt, 2 Bytes pro Zeichen.
Warum passiert das? Sehen wir uns an, wie UTF-8 Zeichencodes darstellt:

Direkt zur Darstellung von Zahlen werden hier mit dem Symbol gekennzeichnete Bits verwendet x. Es ist ersichtlich, dass in einem Zwei-Byte-Datensatz nur 11 solcher Bits (von 16) vorhanden sind. Die fĂŒhrenden Bits haben hier nur eine Hilfsfunktion. Im Fall eines Vier-Byte-Datensatzes werden 21 von 32 Bits fĂŒr die Codepunktnummer reserviert â es scheint, dass drei Bytes (was insgesamt 24 Bits ergibt) ausreichen wĂŒrden, aber Service-Marker fressen zu viel.
Ist das schlecht? Nicht wirklich. Wenn uns der Speicherplatz sehr am Herzen liegt, verfĂŒgen wir einerseits ĂŒber Komprimierungsalgorithmen, mit denen sich die zusĂ€tzliche Entropie und Redundanz problemlos beseitigen lĂ€sst. Andererseits bestand das Ziel von Unicode darin, eine möglichst universelle Codierung bereitzustellen. Beispielsweise können wir eine in UTF-8 codierte Zeile einem Code anvertrauen, der zuvor nur mit ASCII funktionierte, und mĂŒssen keine Angst haben, dass er ein Zeichen aus dem ASCII-Bereich sieht, das tatsĂ€chlich nicht vorhanden ist (schlieĂlich in UTF-8 alle). Bytes beginnend mit dem Nullbit â genau das ist ASCII). Und wenn wir plötzlich einen kleinen Schwanz von einer langen Zeichenfolge abschneiden möchten, ohne ihn von Anfang an zu dekodieren (oder einen Teil der Informationen nach einem beschĂ€digten Abschnitt wiederherstellen möchten), ist es fĂŒr uns einfach, den Versatz zu finden, an dem ein Zeichen beginnt (es reicht aus). um Bytes zu ĂŒberspringen, die ein Bit-PrĂ€fix haben 10).
Warum dann etwas Neues erfinden?
Gleichzeitig gibt es gelegentlich Situationen, in denen Komprimierungsalgorithmen wie Deflate schlecht anwendbar sind, Sie aber eine kompakte Speicherung von Zeichenfolgen erreichen möchten. Persönlich bin ich auf dieses Problem gestoĂen, als ich ĂŒber das Bauen nachdachte fĂŒr ein groĂes Wörterbuch mit Wörtern in beliebigen Sprachen. Einerseits ist jedes Wort sehr kurz, sodass eine Komprimierung wirkungslos ist. Andererseits war die von mir in Betracht gezogene Baumimplementierung so konzipiert, dass jedes Byte der gespeicherten Zeichenfolge einen separaten Baumscheitelpunkt generierte, sodass die Minimierung ihrer Anzahl sehr nĂŒtzlich war. In meiner Bibliothek (Wie in , auf dem es basiert) kann ein Ă€hnliches Problem einfach gelöst werden â in Strings gepackt -Wörterbuch, dort gespeichert . Dies funktioniert jedoch, wie leicht zu verstehen ist, nur fĂŒr ein begrenztes Alphabet gut â eine Zeile auf Chinesisch kann einem solchen Wörterbuch nicht hinzugefĂŒgt werden.
UnabhĂ€ngig davon möchte ich auf eine weitere unangenehme Nuance hinweisen, die bei der Verwendung von UTF-8 in einer solchen Datenstruktur auftritt. Das Bild oben zeigt, dass, wenn ein Zeichen als zwei Bytes geschrieben wird, die Bits, die sich auf seine Nummer beziehen, nicht in einer Reihe stehen, sondern durch ein Bitpaar getrennt sind 10 mitten drin: 110xxxxx 10xxxxxx. Aus diesem Grund kommt es zu einem Ăberlauf der unteren 6 Bits des zweiten Bytes im Zeichencode (d. h. es kommt zu einem Ăbergang). 10111111 â 10000000), dann Ă€ndert sich auch das erste Byte. Es stellt sich heraus, dass der Buchstabe âpâ durch Bytes bezeichnet wird 0xD0 0xBF, und das nĂ€chste ârâ ist schon 0xD1 0x80. In einem PrĂ€fixbaum fĂŒhrt dies zur Aufteilung des ĂŒbergeordneten Knotens in zwei Knoten â einen fĂŒr das PrĂ€fix 0xD0, und noch eins fĂŒr 0xD1 (obwohl das gesamte kyrillische Alphabet nur durch das zweite Byte kodiert werden konnte).
Was habe ich bekommen
Angesichts dieses Problems beschloss ich, das Spielen mit Bits zu ĂŒben und mich gleichzeitig etwas besser mit der Struktur von Unicode als Ganzes vertraut zu machen. Das Ergebnis war das UTF-C-Kodierungsformat (âCâ fĂŒr kompakt), der nicht mehr als 3 Bytes pro Codepunkt ausgibt und sehr oft nur Ausgaben ermöglicht ein zusĂ€tzliches Byte fĂŒr die gesamte codierte Zeile. Dies fĂŒhrt dazu, dass sich bei vielen Nicht-ASCII-Alphabeten eine solche Kodierung herausstellt 30â60 % kompakter als UTF-8.
Ich habe Beispiele fĂŒr die Implementierung von Kodierungs- und Dekodierungsalgorithmen im Formular vorgestellt , Sie können sie frei in Ihrem Code verwenden. Ich möchte jedoch dennoch betonen, dass dieses Format in gewisser Weise ein âFahrradâ bleibt und ich die Verwendung nicht empfehle ohne zu wissen, warum Sie es brauchen. Dies ist immer noch eher ein Experiment als eine ernsthafte âVerbesserung von UTF-8â. Dennoch ist der Code dort sauber und prĂ€gnant geschrieben, mit vielen Kommentaren und Testabdeckungen.

Testergebnisse und Vergleich mit UTF-8
Das habe ich auch getan , wo Sie die Leistung des Algorithmus bewerten können, und dann werde ich Ihnen mehr ĂŒber seine Prinzipien und den Entwicklungsprozess erzĂ€hlen.
Eliminierung redundanter Bits
Als Basis habe ich natĂŒrlich UTF-8 genommen. Die erste und offensichtlichste Sache, die daran geĂ€ndert werden kann, besteht darin, die Anzahl der Dienstbits in jedem Byte zu reduzieren. Beispielsweise beginnt das erste Byte in UTF-8 immer mit entweder 0, oder mit 11 - ein PrĂ€fix 10 Nur die folgenden Bytes haben es. Ersetzen wir das PrĂ€fix 11 auf 1, und fĂŒr die nĂ€chsten Bytes werden wir die PrĂ€fixe vollstĂ€ndig entfernen. Was wird passieren?
0xxxxxxx â 1 Byte
10xxxxxx xxxxxxxx - 2 Bytes
110xxxxx xxxxxxxx xxxxxxxx - 3 Bytes
Moment, wo ist der Vier-Byte-Datensatz? Aber es wird nicht mehr benötigt â beim Schreiben in drei Bytes stehen uns jetzt 21 Bit zur VerfĂŒgung und das reicht fĂŒr alle Zahlen bis 0x10FFFF.
Was haben wir hier geopfert? Das Wichtigste ist die Erkennung von Zeichengrenzen an einer beliebigen Stelle im Puffer. Wir können nicht auf ein beliebiges Byte zeigen und daraus den Anfang des nÀchsten Zeichens finden. Dies ist eine EinschrÀnkung unseres Formats, in der Praxis ist dies jedoch selten erforderlich. Normalerweise können wir den Puffer von Anfang an durchfahren (besonders wenn es um kurze Leitungen geht).
Auch die Situation bei der Abdeckung von Sprachen mit 2 Bytes hat sich verbessert: Das Zwei-Byte-Format ergibt nun einen Bereich von 14 Bits, und das sind Codes bis zu 0x3FFF. Die Chinesen haben Pech (ihre Schriftzeichen reichen meist von 0x4E00 auf 0x9FFF), aber Georgier und viele andere Völker haben mehr SpaĂ â ihre Sprachen passen auch in 2 Bytes pro Zeichen.
Geben Sie den Encoder-Status ein
Denken wir nun ĂŒber die Eigenschaften der Linien selbst nach. Das Wörterbuch enthĂ€lt am hĂ€ufigsten Wörter, die mit Buchstaben desselben Alphabets geschrieben sind, und das gilt auch fĂŒr viele andere Texte. Es wĂ€re gut, dieses Alphabet einmal anzugeben und dann nur die Nummer des darin enthaltenen Buchstabens anzugeben. Mal sehen, ob uns die Anordnung der Zeichen in der Unicode-Tabelle hilft.
Wie oben erwĂ€hnt, ist Unicode unterteilt in das Flugzeug Jeweils 65536 Codes. Dies ist jedoch keine sehr nĂŒtzliche Unterteilung (wie bereits gesagt, meistens befinden wir uns in der Nullebene). Interessanter ist die Division durch Blöcke. Diese Bereiche haben keine feste LĂ€nge mehr und sind aussagekrĂ€ftiger â in der Regel fassen sie jeweils Zeichen aus demselben Alphabet zusammen.

Ein Block, der Zeichen des bengalischen Alphabets enthĂ€lt. Leider ist dies aus historischen GrĂŒnden ein Beispiel fĂŒr eine nicht sehr dichte Verpackung â 96 Zeichen sind chaotisch ĂŒber 128 Blockcodepunkte verteilt.
Die AnfĂ€nge von Blöcken und ihre GröĂe sind immer ein Vielfaches von 16 â dies dient lediglich der Bequemlichkeit. DarĂŒber hinaus beginnen und enden viele Blöcke mit Werten, die ein Vielfaches von 128 oder sogar 256 sind â das grundlegende kyrillische Alphabet nimmt beispielsweise 256 Bytes ein 0x0400 auf 0x04FF. Das ist ganz praktisch: Wenn wir das PrĂ€fix einmal speichern 0x04, dann kann jedes kyrillische Zeichen in ein Byte geschrieben werden. Auf diese Weise verlieren wir zwar die Möglichkeit, zu ASCII (und zu allen anderen Zeichen im Allgemeinen) zurĂŒckzukehren. Deshalb machen wir Folgendes:
- Zwei Bytes
10yyyyyy yxxxxxxxBezeichnen Sie ein Symbol nicht nur mit einer Zahlyyyyyy yxxxxxxx, sondern auch Ă€ndern aktuelles Alphabet aufyyyyyy y0000000(d. h. wir merken uns alle Bits auĂer den niedrigstwertigen 7-Bit); - Ein Byte
0xxxxxxxDies ist das Zeichen des aktuellen Alphabets. Es muss lediglich zu dem Offset hinzugefĂŒgt werden, den wir uns in Schritt 1 gemerkt haben. Obwohl wir das Alphabet nicht geĂ€ndert haben, ist der Offset Null, sodass wir die KompatibilitĂ€t mit ASCII beibehalten haben.
Ebenso fĂŒr Codes, die 3 Bytes erfordern:
- Drei Bytes
110yyyyy yxxxxxxx xxxxxxxxGeben Sie ein Symbol mit einer Zahl anyyyyyy yxxxxxxx xxxxxxxx, Ă€ndern aktuelles Alphabet aufyyyyyy y0000000 00000000(erinnerte sich an alles auĂer an die JĂŒngeren 15-Bit) und aktivieren Sie das KontrollkĂ€stchen, in dem wir uns gerade befinden lang Modus (wenn wir das Alphabet wieder in ein Doppelbyte-Alphabet Ă€ndern, setzen wir dieses Flag zurĂŒck); - Zwei Bytes
0xxxxxxx xxxxxxxxim Langmodus ist es das Zeichen des aktuellen Alphabets. Ebenso fĂŒgen wir es mit dem Offset aus Schritt 1 hinzu. Der einzige Unterschied besteht darin, dass wir jetzt zwei Bytes lesen (da wir in diesen Modus gewechselt sind).
Hört sich gut an: WĂ€hrend wir jetzt Zeichen aus demselben 7-Bit-Unicode-Bereich codieren mĂŒssen, geben wir am Anfang 1 zusĂ€tzliches Byte und insgesamt ein Byte pro Zeichen aus.

Arbeitet mit einer der frĂŒheren Versionen. Es schlĂ€gt UTF-8 bereits oft, aber es gibt noch Raum fĂŒr Verbesserungen.
Was ist schlimmer? Erstens haben wir eine Bedingung, nĂ€mlich aktueller Alphabet-Offset und KontrollkĂ€stchen Langer Modus. Dies schrĂ€nkt uns noch weiter ein: Jetzt können dieselben Zeichen in verschiedenen Kontexten unterschiedlich codiert werden. Die Suche nach Teilzeichenfolgen muss beispielsweise unter BerĂŒcksichtigung dieser Tatsache erfolgen und nicht nur durch den Vergleich von Bytes. Zweitens, sobald wir das Alphabet geĂ€ndert haben, wurde es mit der Kodierung von ASCII-Zeichen schlecht (und das ist nicht nur das lateinische Alphabet, sondern auch grundlegende Zeichensetzung, einschlieĂlich Leerzeichen) â sie erfordern eine erneute Ănderung des Alphabets auf 0, d. h. noch einmal ein zusĂ€tzliches Byte (und dann noch eines, um zu unserem Hauptpunkt zurĂŒckzukommen).
Ein Alphabet ist gut, zwei sind besser
Versuchen wir, unsere Bit-PrĂ€fixe ein wenig zu Ă€ndern, indem wir zu den drei oben beschriebenen noch eines hinzufĂŒgen:
0xxxxxxx â 1 Byte im Normalmodus, 2 im Langmodus
11xxxxxx â 1 Byte
100xxxxx xxxxxxxx - 2 Bytes
101xxxxx xxxxxxxx xxxxxxxx - 3 Bytes

Jetzt ist in einem Zwei-Byte-Datensatz ein Bit weniger verfĂŒgbar â Code zeigt bis zu 0x1FFFUnd nicht 0x3FFF. Allerdings ist er immer noch merklich gröĂer als bei Doppelbyte-UTF-8-Codes, die meisten gĂ€ngigen Sprachen passen noch rein, der auffĂ€lligste Verlust ist ausgefallen Đž , die Japaner sind traurig.
Was ist dieser neue Code? 11xxxxxx? Dies ist ein kleiner âVorratâ mit einer GröĂe von 64 Zeichen. Er ergĂ€nzt unser Hauptalphabet, daher habe ich ihn Hilfsalphabet genannt (Hilfs-) Alphabet. Wenn wir das aktuelle Alphabet wechseln, wird ein Teil des alten Alphabets zum Hilfsalphabet. Wir haben zum Beispiel von ASCII auf Kyrillisch umgestellt â der Stash enthĂ€lt jetzt 64 Zeichen Lateinisches Alphabet, Zahlen, Leerzeichen und Komma (hĂ€ufigste EinfĂŒgungen in Nicht-ASCII-Texten). Wechseln Sie zurĂŒck zu ASCII â und der Hauptteil des kyrillischen Alphabets wird zum Hilfsalphabet.
Dank des Zugriffs auf zwei Alphabete können wir eine groĂe Anzahl von Texten mit minimalen Kosten fĂŒr den Wechsel des Alphabets verarbeiten (Interpunktion fĂŒhrt meistens zu einer RĂŒckkehr zu ASCII, aber danach erhalten wir viele Nicht-ASCII-Zeichen aus dem zusĂ€tzlichen Alphabet, ohne). erneut umschalten).
Bonus: Voranstellen des Unteralphabets 11xxxxxx und die Wahl seines anfÀnglichen Offsets 0xC0erhalten wir teilweise KompatibilitÀt mit CP1252. Mit anderen Worten: Viele (aber nicht alle) in CP1252 kodierte westeuropÀische Texte sehen in UTF-C gleich aus.
Hier entsteht jedoch eine Schwierigkeit: Wie erhĂ€lt man ein Hilfsalphabet aus dem Hauptalphabet? Sie können den gleichen Offset belassen, aber leider spielt hier die Unicode-Struktur bereits gegen uns. Sehr oft steht der Hauptteil des Alphabets nicht am Anfang des Blocks (zum Beispiel steht der Code im russischen GroĂbuchstaben âAâ) 0x0410, obwohl der kyrillische Block mit beginnt 0x0400). Nachdem wir also die ersten 64 Zeichen in den Vorrat aufgenommen haben, verlieren wir möglicherweise den Zugriff auf den hinteren Teil des Alphabets.
Um dieses Problem zu beheben, habe ich einige Blöcke, die verschiedenen Sprachen entsprechen, manuell durchgegangen und fĂŒr sie den Versatz des Hilfsalphabets innerhalb des Hauptalphabets angegeben. Ausnahmsweise wurde das lateinische Alphabet generell wie base64 neu geordnet.

Letzter Schliff
Lasst uns endlich darĂŒber nachdenken, wo wir sonst noch etwas verbessern können.
Beachten Sie, dass das Format 101xxxxx xxxxxxxx xxxxxxxx ermöglicht die Kodierung von Zahlen bis zu 0x1FFFFF, und Unicode endet frĂŒher, um 0x10FFFF. Mit anderen Worten, der letzte Codepunkt wird dargestellt als 10110000 11111111 11111111. Daher können wir sagen, dass das erste Byte die Form hat 1011xxxx (wo xxxx gröĂer als 0), dann bedeutet es etwas anderes. Dort kann man zum Beispiel weitere 15 Zeichen hinzufĂŒgen, die stĂ€ndig in einem Byte zum Kodieren zur VerfĂŒgung stehen, aber ich habe mich entschieden, es anders zu machen.
Schauen wir uns jetzt die Unicode-Blöcke an, die drei Bytes benötigen. Im Grunde handelt es sich, wie bereits erwĂ€hnt, um chinesische Schriftzeichen â aber mit ihnen lĂ€sst sich kaum etwas anfangen, es gibt 21 davon. Aber auch Hiragana und Katakana flogen dorthin â und davon gibt es nicht mehr so ââviele, weniger als zweihundert. Und da wir uns an die Japaner erinnern, gibt es auch Emojis (tatsĂ€chlich sind sie in Unicode an vielen Stellen verstreut, aber die Hauptblöcke liegen im Bereich 0x1F300 - 0x1FBFF). Wenn Sie darĂŒber nachdenken, dass es mittlerweile Emojis gibt, die aus mehreren Codepunkten gleichzeitig zusammengesetzt sind (zum Beispiel das Emoji âââ besteht aus bis zu 7 Codes!), dann ist es eine absolute Schande, fĂŒr jeden drei Bytes auszugeben (7Ă3 = 21 Bytes fĂŒr ein Symbol, ein Albtraum).
Daher wÀhlen wir einige ausgewÀhlte Bereiche aus, die Emoji, Hiragana und Katakana entsprechen, nummerieren sie in einer fortlaufenden Liste neu und kodieren sie als zwei statt drei Bytes:
1011xxxx xxxxxxxx
Toll: das oben erwĂ€hnte âââ-Emoji, bestehend aus 7 Codepunkten, benötigt 8 Bytes in UTF-25, und wir passen es ein 14 (genau zwei Bytes fĂŒr jeden Codepunkt). Habr weigerte sich ĂŒbrigens, es zu verdauen (sowohl im alten als auch im neuen Editor), also musste ich es mit einem Bild einfĂŒgen.
Versuchen wir, ein weiteres Problem zu beheben. Wie wir uns erinnern, besteht das Grundalphabet im Wesentlichen aus hohe 6 Bit, die wir uns merken und an den Code jedes nĂ€chsten dekodierten Symbols kleben. Bei chinesischen Schriftzeichen, die im Block stehen 0x4E00 - 0x9FFF, das ist entweder Bit 0 oder 1. Das ist nicht sehr praktisch: Wir mĂŒssen das Alphabet stĂ€ndig zwischen diesen beiden Werten wechseln (d. h. drei Bytes ausgeben). Beachten Sie jedoch, dass wir im langen Modus vom Code selbst die Anzahl der Zeichen abziehen können, die wir im kurzen Modus codieren (nach all den oben beschriebenen Tricks sind dies 10240) â dann verschiebt sich der Bereich der Hieroglyphen 0x2600 - 0x77FF, und in diesem Fall sind im gesamten Bereich die höchstwertigen 6 Bits (von 21) gleich 0. Somit verwenden Hieroglyphensequenzen zwei Bytes pro Hieroglyphe (was fĂŒr einen so groĂen Bereich optimal ist), ohne was zu Alphabetwechseln fĂŒhrt.
Alternative Lösungen: SCSU, BOCU-1
Unicode-Experten, die gerade den Titel des Artikels gelesen haben, werden Sie höchstwahrscheinlich daran erinnern, dass es direkt unter den Unicode-Standards gibt (SCSU), das eine Codierungsmethode beschreibt, die der im Artikel beschriebenen sehr Àhnlich ist.
Ich gebe ehrlich zu: Ich habe von seiner Existenz erst erfahren, als ich tief in die Niederschrift meiner Entscheidung vertieft war. HÀtte ich es von Anfang an gewusst, hÀtte ich wahrscheinlich versucht, eine Implementierung zu schreiben, anstatt einen eigenen Ansatz zu entwickeln.
Interessant ist, dass SCSU Ideen verwendet, die denen sehr Ă€hnlich sind, die ich mir selbst ausgedacht habe (anstelle des Konzepts der âAlphabeteâ verwenden sie âFensterâ, und davon sind mehr verfĂŒgbar als ich). Gleichzeitig hat dieses Format auch Nachteile: Es Ă€hnelt Komprimierungsalgorithmen etwas nĂ€her als Kodierungsalgorithmen. Insbesondere gibt der Standard viele Darstellungsmethoden an, sagt aber nicht, wie man die optimale auswĂ€hlt â dafĂŒr muss der Encoder eine Art Heuristik verwenden. Daher ist ein SCSU-Encoder, der eine gute Verpackung erzeugt, komplexer und umstĂ€ndlicher als mein Algorithmus.
Zum Vergleich habe ich eine relativ einfache SCSU-Implementierung auf JavaScript ĂŒbertragen â in Bezug auf das Codevolumen erwies es sich als vergleichbar mit meinem UTF-C, aber in einigen FĂ€llen war das Ergebnis um Dutzende Prozent schlechter (manchmal kann es sogar darĂŒber hinausgehen, aber nicht viel). Beispielsweise wurden Texte in HebrĂ€isch und Griechisch mit UTF-C kodiert 60 % besser als SCSU (wahrscheinlich aufgrund ihrer kompakten Alphabete).
UnabhĂ€ngig davon möchte ich hinzufĂŒgen, dass es neben SCSU auch eine andere Möglichkeit gibt, Unicode kompakt darzustellen: , aber es zielt auf MIME-KompatibilitĂ€t ab (die ich nicht brauchte) und verfolgt einen etwas anderen Ansatz bei der Kodierung. Ich habe die Wirksamkeit nicht beurteilt, aber es scheint mir, dass sie wahrscheinlich nicht höher sein wird als die von SCSU.
Mögliche Verbesserungen
Der von mir vorgestellte Algorithmus ist von Natur aus nicht universell (hier weichen meine Ziele wahrscheinlich am meisten von den Zielen des Unicode-Konsortiums ab). Ich habe bereits erwĂ€hnt, dass es hauptsĂ€chlich fĂŒr eine Aufgabe entwickelt wurde (Speichern eines mehrsprachigen Wörterbuchs in einem PrĂ€fixbaum) und dass einige seiner Funktionen fĂŒr andere Aufgaben möglicherweise nicht gut geeignet sind. Aber die Tatsache, dass es sich nicht um einen Standard handelt, kann ein Pluspunkt sein â Sie können es ganz einfach an Ihre BedĂŒrfnisse anpassen.
Auf die offensichtliche Weise können Sie beispielsweise das Vorhandensein von ZustĂ€nden beseitigen und eine zustandslose Codierung durchfĂŒhren â nur Variablen nicht aktualisieren offs, auxOffs Đž is21Bit im Encoder und Decoder. In diesem Fall ist es nicht möglich, Zeichenfolgen desselben Alphabets effektiv zu packen, aber es besteht die Garantie, dass dasselbe Zeichen unabhĂ€ngig vom Kontext immer mit denselben Bytes codiert wird.
DarĂŒber hinaus können Sie den Encoder an eine bestimmte Sprache anpassen, indem Sie den Standardstatus Ă€ndern. Wenn Sie sich beispielsweise auf russische Texte konzentrieren, stellen Sie den Encoder und Decoder am Anfang ein offs = 0x0400 Đž auxOffs = 0. Dies ist insbesondere im Stateless-Modus sinnvoll. Im Allgemeinen Ă€hnelt dies der Verwendung der alten XNUMX-Bit-Kodierung, ohne dass jedoch die Möglichkeit entfernt wird, bei Bedarf Zeichen aus dem gesamten Unicode einzufĂŒgen.
Ein weiterer bereits erwĂ€hnter Nachteil besteht darin, dass es in groĂen, in UTF-C codierten Texten keine schnelle Möglichkeit gibt, die Zeichengrenze zu finden, die einem beliebigen Byte am nĂ€chsten liegt. Wenn Sie beispielsweise die letzten 100 Bytes aus dem codierten Puffer abschneiden, besteht die Gefahr, dass Sie MĂŒll erhalten, mit dem Sie nichts anfangen können. Die Kodierung ist nicht fĂŒr die Speicherung von Multi-Gigabyte-Protokollen ausgelegt, aber im Allgemeinen kann dies korrigiert werden. Byte 0xBF darf niemals als erstes Byte erscheinen (kann aber das zweite oder dritte sein). Daher können Sie beim Codieren die Sequenz einfĂŒgen 0xBF 0xBF 0xBF alle, sagen wir, 10 KB â wenn Sie dann eine Grenze finden mĂŒssen, reicht es aus, das ausgewĂ€hlte StĂŒck zu scannen, bis eine Ă€hnliche Markierung gefunden wird. Im Anschluss an den letzten 0xBF ist garantiert der Anfang eines Zeichens. (Beim Dekodieren muss diese Folge von drei Bytes natĂŒrlich ignoriert werden.)
Zusammenfassend
Wenn Sie bis hierhin gelesen haben, herzlichen GlĂŒckwunsch! Ich hoffe, dass Sie, wie ich, etwas Neues ĂŒber die Struktur von Unicode gelernt (oder Ihr GedĂ€chtnis aufgefrischt) haben.

Demoseite. Das Beispiel HebrĂ€isch zeigt die Vorteile gegenĂŒber UTF-8 und SCSU.
Die oben beschriebene Forschung sollte nicht als Eingriff in die Standards angesehen werden. Generell bin ich jedoch mit den Ergebnissen meiner Arbeit zufrieden und daher zufrieden : Beispielsweise wiegt eine minimierte JS-Bibliothek nur 1710 Bytes (und hat natĂŒrlich keine AbhĂ€ngigkeiten). Wie ich oben erwĂ€hnt habe, ist ihre Arbeit unter zu finden (Es gibt auch eine Reihe von Texten, anhand derer es mit UTF-8 und SCSU verglichen werden kann).
AbschlieĂend möchte ich noch einmal auf FĂ€lle hinweisen, in denen UTF-C verwendet wird lohnt sich nicht:
- Wenn Ihre Zeilen lang genug sind (von 100 bis 200 Zeichen). In diesem Fall sollten Sie ĂŒber die Verwendung von Komprimierungsalgorithmen wie Deflate nachdenken.
- Wenn Sie brauchen ASCII-Transparenz, das heiĂt, es ist fĂŒr Sie wichtig, dass die codierten Sequenzen keine ASCII-Codes enthalten, die nicht im Originalstring enthalten waren. Dies kann vermieden werden, wenn Sie bei der Interaktion mit APIs von Drittanbietern (z. B. beim Arbeiten mit einer Datenbank) das Codierungsergebnis als abstrakte Menge von Bytes und nicht als Zeichenfolgen ĂŒbergeben. Andernfalls besteht die Gefahr unerwarteter SicherheitslĂŒcken.
- Wenn Sie in der Lage sein möchten, Zeichengrenzen mit einem beliebigen Versatz schnell zu finden (z. B. wenn ein Teil einer Zeile beschĂ€digt ist). Dies ist möglich, jedoch nur durch Scannen der Zeile von Anfang an (oder durch Anwenden der im vorherigen Abschnitt beschriebenen Ănderung).
- Wenn Sie schnell Operationen am Inhalt von Zeichenfolgen durchfĂŒhren mĂŒssen (sortieren, nach Teilzeichenfolgen darin suchen, verketten). Dies erfordert, dass Zeichenfolgen zuerst dekodiert werden, sodass UTF-C in diesen FĂ€llen langsamer als UTF-8 ist (aber schneller als Komprimierungsalgorithmen). Da die gleiche Zeichenfolge immer auf die gleiche Weise codiert wird, ist ein genauer Vergleich der Decodierung nicht erforderlich und kann byteweise erfolgen.
Update: Benutzer hat eine Grafik gepostet, die die Anwendbarkeitsgrenzen von UTF-C hervorhebt. Es zeigt, dass UTF-C effizienter ist als ein Allzweck-Komprimierungsalgorithmus (eine Variation von LZW), solange die gepackte Zeichenfolge kĂŒrzer ist ~140 Zeichen (Ich stelle jedoch fest, dass der Vergleich an einem Text durchgefĂŒhrt wurde; bei anderen Sprachen kann das Ergebnis abweichen).

Source: habr.com
