Почему проблема порядка байтов есть в UTF-16, но её нет в UTF-8?
1,00
р.
р.
Я прочитал, что в UTF-16 два различных порядка байтов (endianness) появились потому, что два различных порядка байтов существуют в архитектуре процессоров: Систему, совместимую с процессорами x86, называют little endian, а с процессорами m68k и SPARC — big endian. То есть одно и то же число 0x1234ABCD кодируется последовательностью байтов: little endian: 12 34 56 78 big endian: 78 56 34 12 Соответственно, при раскодировании последовательности байт в последовательность чисел (или code point'ов юникода) нужно учитывать использованный при кодировании порядок байтов. (Это несколько дилетантское утверждение, но лучше сформулировать я пока не могу). Например, если мы кодируем "Привет 😃" в UTF-16: # big endian: П р и в е т ( ) 😃 04 1F 04 40 04 38 04 32 04 35 04 42 00 20 D8 3D DE 03 # little endian: П р и в е т ( ) 😃 1F 04 40 04 38 04 32 04 35 04 42 04 20 00 03 DE D3 D8 Вроде бы всё очевидно. Мы сопоставляем code point'y некоторое число согласно алгоритму кодировки, а потом записываем это число в соответствии с порядком байт, принятым в системе. Теперь UTF-8: П р и в е т ( ) 😃 D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82 20 F0 9F 98 83 # в двоичной системе счисления: 11010000 10011111 11010001 10000000 11010000 10111000 11010000 10110010 11010000 10110101 11010001 10000010 # по первому биту сразу видно, что этот code point закодирован одним байтом 00100000 # а здесь первый байт начинается с 4 единиц, значит будет 3 trailing byte'а 11110000 10011111 10011000 10000011 Алгоритм кодировки поменялся, но архитектура процессора осталась прежней! Мы по прежнему получаем число, которое занимает от 1 до 4х байт. Почему с UTF-8 нас не беспокоит, что байты будут записаны вот так? П р и в е т ( ) 😃 9F D0 80 D1 B8 D0 B2 D0 B5 D0 82 D1 20 83 98 9F F0
Дополнение: Задавая этот вопрос, я уже знал, что UTF-8 использует однобайтовые code unit'ы, а UTF-16 – двухбайтовые. Попробую уточнить, что мне было непонятно. Есть символ «😃». При кодировании его в алгоритме UTF-8 получается последовательность байт F0 9F 98 83. Это тоже число, четырёхбайтовое слово, его можно использовать для сравнения или сортировки строк, закодированных в UTF-8 (правда, толку от такой сортировки немного). В вышеуказанном виде оно имеет порядок big-endian, значит системы с архитектурой big-endian могут получить преимущество в работе с ним. Но что с little-endian? Как там будет происходить сравнение? Для примера, будем сравнивать «😃» (F0 9F 98 83) и «😐» (F0 9F 98 90). У меня есть два предположения: Big-endian системы работают с закодированными в UTF-8 символами, как с 1, 2, 3, 4-байтными словами и получают преимущество в скорости операций. То есть, в них достаточно сравнить F09F9883 и F09F9890 как четырехбайтовые слова. Little-endian системы вынуждены сравнивать побайтно или переворачивать слово дважды. Любая архитектура работает с закодированными в UTF-8 символами строго как с последовательностями байт, не оперируя словами более 1 байта. То есть, сравниваются пары байт: FO и FO, 9F и 9F, 98 и 98, 83 и 90. При этом теряется потенциальное преимущество от сравнения двух слов, зато для любой архитектуры алгоритм работает одинаково.
Ответ Дело в том, что UTF-8 и UTF-16 обычно хранится в памяти нераспакованным, в том же виде, как он приходит в потоке (например, в файле). [Ну и если он таки распаковывается, то это рассмотрение играет роль в момент распаковки.] Само по себе хранение никакой проблемы, понятно, не создаёт. Проблему создаёт обработка, например, сравнение символов. В UTF-8 вы читаете входной поток по байту, и интерпретируете их последовательно. Соответственно получившееся значение code point получается однозначно и не зависит от порядка байтов машины: результат приведения к code point однозначно определён, и при сравнении используется именно он. А вот в UTF-16 вы читаете входной поток по два байта, и для сравнения в обычном случае вовсе не нужно вычислять code point. Если у вас есть двухбайтное слово в нативной кодировке, не соответствующее суррогатной паре (а это основной, самый частый случай), то для сравнение можно просто использовать её значение, она равна своему code point. Но если кодировка не нативная, вам понадобится переставить байты. Если бы в UTF-16 был задан конкретный порядок байт, составляющих двойной байт (и тем самым задана endianness), то платформы, на которых данный порядок не является нативным, оказались бы в проигрыше: они должны были бы совершать дополнительные действия (перестановку байт) при чтении и записи потока. С двумя вариантами кодировки приложения могут пользоваться тем форматом, который нативен на их платформе, получая тем самым выигрыш в скорости. Держать байты в памяти в ненативном порядке — плохая идея: их получается намного затратнее сортировать и сравнивать. С нативным порядком в обычном случае нужна лишь проверка на суррогатную пару, а с ненативным ещё и перестановка байтов. Например, для сравнения 1C 55 и 1B 77 в big endian-смысле на little endian-системе не обойтись без перестановки байт. Потому что если сравнивать без перестановки, то будут сравниваться 0x551C и 0x771B, и результат будет неверным. То же и для сортировки.
Обновление ответа к обновлению вопроса. Насколько я понимаю, при обработке UTF-8 мы не знаем наперёд, сколько байт будет занимать тот или иной символ. Поэтому мы вынуждены работать с потоком байт, а не потоком нативных слов. Если бы мы знали, что наш символ всегда кодируется четырьмя байтами, мы могли бы или просто сравнить нативным образом,или для неподходящей байтоориентации скопировать оба четырёхбайтных слова во временные переменные, развернуть их и сравнить нативным сравнением четырёхбайтных слов. Но этому мешает ещё и то, что наши четыре байта находятся на случайной позиции в потоке, и значит, скорее всего не выровнены на границу 4-ёх байт. На многих архитектурах (кроме, однако, x86) такой доступ не разрешён, и придётся «выковыривать» байты по частям. Таким образом, получается проще и эффективнее просто сравнить байты по одному. В UTF-16, кстати, возможных случаев меньше: там возможен либо символ из одного codepoint'а, который можно сравнивать нативно или с одним разворотом, если не угадали с порядком байт, либо из двух (где наверное лучше снова-таки сравнить два раза по двухбайтному слову).