Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Почему проблема порядка байтов есть в UTF-16, но её нет в UTF-8?


Я прочитал, что в 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. При этом теряется потенциальное преимущество от сравнения двух слов, зато для любой архитектуры алгоритм работает одинаково.

    


Ответы

Ответ 1



Дело в том, что 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'а который можно сравнивать нативно или с одним разворотом, если не угадали с порядком байт, либо из двух (где наверное лучше снова-таки сравнить два раза по двухбайтному слову).

Ответ 2



Почему проблема порядка байтов есть в UTF-16, но её нет в UTF-8? Потому что code unit равен 8 битам (одному байту) в UTF-8 и 16 битам (двум байтам в UTF-16. В зависимости от порядка байт внутри code unit, есть utf-16le и utf-16be кодировки, которые могут быть использованы на одном и том же компьютере вне зависимости от endianness CPU): >>> 'я'.encode('utf-16le').hex() '4f04' >>> 'я'.encode('utf-16be').hex() '044f' Символ я (U+44f) кодируется в UTF-16 в одно и то же 16-битное число: 1103 == 0x44f что для utf-16 совпадает с номером (Unicode code point) символа в Юникоде (для BMP символов). Само 16-битное число может быть в памяти представлено в виде двух байт: 4f 04 (от младшего к старшему порядок байт) или 04 4f (от старшего к младшему порядок байт). >>> 'я'.encode('utf-8').hex() 'd18f' я (U+44f) кодируется в UTF-8, используя два 8-битных числа 209 == 0xd1 и 143 = 0x8f. В общем случае UTF-8 может использовать от 1 до 4 октетов (8-битных чисел) для каждого символа (Unicode code point). >>> '😂'.encode('utf-16le').hex() '3dd802de' >>> '😂'.encode('utf-16be').hex() 'd83dde02' >>> '😂'.encode('utf-8').hex() 'f09f9882' Символ 😂 (U+1f602) кодируется в utf-16, используя два 16-битных слова (utf-16 cod units): 0xd83d и 0xde02 (utf-16 суррогатная пара). Представление слова в виде байт зависит от выбранного порядка байт (le, be), но порядок самих слов не меняется. 😂 (U+1f602) кодируется в utf-8, используя четыре октета (utf-8 code units): 0xf0 0x9f, 0x98, 0x82. Представление октета в виде 8-битового байта, очевидно, не зависит от порядка (один октет—один байт). Последовательность code units (октеты для utf-8 и 16-битные слова для utf-16), используема для кодирования выбранного символа, однозначно определена выбранной кодировкой—в частности нельзя порядок code units менять как в utf-16 так и в utf-8 кодировках. Оба пункта из дополнения к вопросу у вас неверны. Не нужно путать как результат виде байт представляется при обмене с внешним миром или внутри разных частей в программ (при записи на диск, отсылке по сети, вызове API) и какие инструкции CPU используе для работы с данными, выполняя конкретный алгоритм. То что октеты нельзя переставлять в utf-8 результате, ещё не значит, что фактические алгоритмы не могут работать с бо́льшими единицами. К примеру, memcpy() очевидно сохраняет порядок байт, при этом фактическая реализация может работать c целыми словами (например, с 64-битными словами).

Ответ 3



Сама по себе проблема с endianness возникает из-за разной общепринятой визуализации значений в памяти в и регистрах процессора: Вот как обычно нумеруют байты в регистре процессора: [ 12 34 AB CD ] разряд 3 2 1 0 Старшие разряды пишутся слева - как традиционно сложилось в математике. В то же время в той же математике сложилась традиция рисовать оси и отрезки и прочи множества слева направо. И все представляют себе память как длинный бесконечный массив байт. [ ?? ?? ?? ?? ...] адрес 0 1 2 3 И у разработчиков процессора есть два решения: сохранять 0-й разряд в 0-й адрес, 1-й в 1-й и т.д, пожертвовав визуализацией, н получив выигрыш в скорости и простоте операций типа "вычитать из памяти 0-й байт в 0-й разряд (little-endian)" разворачивать значение при сохранении, ради удобства отладки В рамках одной системы нет никаких проблем. Главное писать значения в память та же, как вы их оттуда читаете - и можно не думать об endiannes. И все идет хорошо, пока вам не надо передать строку на другой компьютер (прямо по сети, или косвенно - как файл). Создатели сетевых протоколов заранее договорилис как передавать отдельные байты. Т.е. если вы передадите с x86 по сети байты 1, 2, 3, 4, 5, 6 - то любая моторолла получит их по сети в порядке 1, 2, 3, 4, 5, 6. Это жестко вбито в стандартах, на всех уровнях, от TCP/IP до Ethernet. А вот насчет передачи пар байт или четверок байт - никаких договоренностей нет. UTF-8 работает с потоком байт. Предположим, вы хотите записать на диск или переслать "привет" по сети. Это, с точки зрения энкодера, выглядит так: передать D0 передать 9F передать D1 передать 80 ... Получающая сторона (стандарт же!) гарантированно прочитает их в том же порядке D0 9F D1 80... Опять же, при записи в память по одному байту никаких разворотов не происходит, и в памяти это же значение представляется в виде [ D0 9F D1 80 ] Так происходит только потому, что в русском (английском) языке принято писать буквы направо, что случайно совпадает с общепринятой визуализацией памяти. Поэтому строку UTF-8 достаточно просто записать в память - и она готова к передаче Это результат договоренностей на уровне сетевых протоколов / протоколов общения с диском - они тоже работают на уровне байт, а не на уровне бит. Ок, теперь нам захотелось передать тот же привет, но в UTF-16: 0x041F 0x0440 0x0438 0x0432 0x0435 0x0442 Энкодер в UTF-16 не заморачивается и передает эти двухбайтовые слова по сети/в память/н диск. По слову за раз. И ожидает что первое слово будет передано/записано первым, второе - вторым и т.д. передать 0x041F передать 0x0440 передать 0x0438 Как они будут записаны / переданы - зависит от endiansess. Для LE процессор не заморачивается [ 1F, 04 ] [ 40, 04 ] [ 38, 04] адрес 0 1 2 3 4 5 Для big endian - он старательно разворачивает каждое слово: [ 04, 1F ] [ 04, 40 ] [ 04, 38] адрес 0 1 2 3 4 5 Договоренностей о передаче двухбайтовых слов по сети нет, как и договоренностей о хранении их на диске. Поэтому в UTF-16 требуется BOM. На самом деле та же проблема существует с битами при использовании UTF-8 (да и вообщ при передаче любых байт куда угодно). Например, вы хотите передать по сети байт D0. Он же 11010000. Вы будете передавать его как 0, 0, 0, 0, 1, 0, 1, 1? Или как 1, 1, 0, 1, 0, 0, 0, 0? Вы с этой проблемой не сталкиваетесь по разным причинам: - Отсутствует необходимость визуализации бит при хранениии. - Закрыт доступ к реальному формату хранения на физическом уровне (память и дис не позволяют адресовать и читать отдельные биты). - Жесткая стандартизация - заранее согласованный порядк передачи бит в рамках байта в конкретном сетевом протоколе позволяет вам работать на уровне байт. Достаточно взять любой способ передачи, в котором насчет порядка бит виден (например, попробуйте сделать железку, которая через COM-порт) - и проблема себя проявит.

Ответ 4



То есть одно и то же число 0x1234ABCD кодируется последовательностью байтов: little endian: 12 34 56 78 big endian: 78 56 34 12 Соответственно, при раскодировании ... Нет никакого раскодирования. Есть два представления числа в памяти. http://ideone.com/wsOkXK #include int main() { volatile int x = 0x1234ABCD; const unsigned char *p = (unsigned char *)&x; printf("%0*X\n", sizeof(int) << 1, x); for (unsigned q=0; q

Ответ 5



В обоих случаях порядок выборки из памяти чисел, кодирующих символ, одинаковый. случае UTF-8 эти числа однобайтовые и проблемы старшего и младшего байтов, по понятно причине, не возникает. Когда же речь заходит об UTF-16, оказывается, что в разных архитектурах порядок записи байтов в память разный. Например, в архитектуре Intel первым идёт младший байт, а в ARM - старший.

Комментариев нет:

Отправить комментарий