Эффективное декодирование длины символа UTF-8 для ненулевого символа в 32-битном регистре

Я сохраняю символ UTF-8 в eax, а позже, при обработке, мне нужно знать, сколько байтов составляет символ.

Я сузил этот вопрос, который сводит к минимуму сдвиги и маски, и хотел знать, не упустил ли я где-нибудь какой-нибудь хитрый трюк?

Вариант 1: грубая сила

    mov     r11, 4      ;   Maximum bytes
    bt      eax, 31     ;   Test 4th MSB
    jc      .exit 
    dec     r11         ;   Lets try 3
    bt      eax, 23     ;   Test 3rd MSB
    jc      .exit 
    dec     r11         ;   Lets try 2
    bt      eax, 15     ;   Test 2nd MSB
    jc      .exit 
    dec     r11         ;   It's straight up ascii (1 byte)
.exit:

Примечание.

  1. У меня было неправильное накопление в регистре eax, на что указывали, ну, все.
  2. И Маргарет, и Ped7g предоставили решения, и я узнал даже больше, чем ожидал.

person Frank C.    schedule 21.12.2016    source источник
comment
Насколько я знаю, вы должны проверять с другого направления, но не совсем ясно, как ваш персонаж находится в EAX с самого начала.   -  person Jester    schedule 21.12.2016
comment
@Jester при чтении в UTF-8 (или ascii) я сдвигаю порядок символов, поэтому, если это UTF-8, управляющий байт (например, 0x11 ......) находится в более высокой позиции, за которым следует продолжение (например, 0x10). ......). Если я понимаю, что вы имеете в виду, было бы более эффективно сначала проверить низкий порядок для ascii или наименее затратного UTF-8?   -  person Frank C.    schedule 21.12.2016
comment
Подождите, а что же тогда в eax, можете привести примеры? Я ожидал что-то вроде 0x89c389c3 вместо ÉÉ (один символ É с началом следующего символа, который также является É и помещается в оставшиеся два байта eax). Что читает ваш код в таком случае? (текст имеет байты c3 89 c3 89) Если бы вы предоставили примеры для каждой длины, было бы лучше проверить любые предложения по ним. (плюс то, как сильно вы стеснены в запасных регистрах)   -  person Ped7g    schedule 21.12.2016
comment
@ Ped7g - я иду на уровне персонажа, у вас их два (например, É, за которым следует второй É). Итак, для одного (1) персонажа: rax = 0x000000000000c3a1   -  person Frank C.    schedule 21.12.2016
comment
Это c3a1 (á) опечатка на вашей стороне, или я все еще не понимаю, как работают ваши преобразования ценности? Если вы идете по символу, что-то уже проанализировало этот символ. Кстати, почему что-то не обеспечивает также длину? Тогда вы анализируете его дважды. Если вы не возражаете, то почему вы беспокоитесь о производительности второго синтаксического анализа, если вы не возражаете против производительности? Я немного смущен этим.   -  person Ped7g    schedule 21.12.2016
comment
@Ped7g - Да, опечатка.. rax = 0x000000000000c389   -  person Frank C.    schedule 21.12.2016
comment
Поскольку вы кодируете это на ассемблере, вам, вероятно, следует оставить символы UTF-8 неупакованными, потому что это быстрый способ их обработки.   -  person Ross Ridge    schedule 21.12.2016
comment
См. также комментарии к этому вопросу для некоторых ссылок на другие SO вопросы по расшифровке UTF-8. (алгоритмы на C легко реализовать на ассемблере).   -  person Peter Cordes    schedule 23.12.2016


Ответы (2)


Если вы можете предположить правильную кодировку символа, вы можете просто проверить, где самый высокий ноль находится в первой кодовой единице (благодаря свойству автосинхронизации UTF-8).

Виновником является то, что для кодовых точек одной кодовой единицы старший нуль равен 7 биту. Для кодовых точек n кодовых единиц старший бит равен 7 - n (обратите внимание на "разрыв").

Предположим, что первая кодовая единица находится в al.

not al                 ;Trasform highest 0 in highest 1
bsr al, al             ;Find the index (from bit0) of the first 1 from the left
xor al, 7              ;Perform 7 - index
                       ;This gives 0 for single code unit code points
mov ah, 1
cmovz al, ah           ;Change back to 1

Обратите внимание, что bsr не определено для ввода 0, но это может произойти только для недопустимой ведущей кодовой единицы (со значением 11111111b).

Вы можете обнаружить недопустимую кодовую единицу 0xff с помощью jz <error handler> после инструкции bsr.

Спасибо @CodyGray за указание на ошибку в оригинальной версии.
Спасибо @PeterCorders за указание на трюк XOR для выполнения 7 - AL.

person Margaret Bloom    schedule 21.12.2016
comment
Хорошо, да... Я неправильно кодирую. Поэтому, когда я это сделаю, первый байт (если не прямой ascii) будет иметь управляющие биты, которые также кодируют длину в битах (на основе 0) 4-6 потенциально (для подсчета от 2 до 4 байтов). Есть ли инструкция по нахождению 0 бита из msb? Если это так, я могу найти это, я могу переключить бит 7, сдвинуть бит индекса вправо и просто использовать значение, поскольку счетчик останется? - person Frank C.; 21.12.2016
comment
@ФранкК. Не могу найти инструкцию по нахождению старшего нулевого бита (первый ноль из MSb). Я решил, что сначала не использовал кодовую единицу, а использовал bsr. Что касается вашего второго вопроса, вам необходимо принять во внимание, что существует разрыв в позиции самого высокого нуля (индекс 7 для символов с одной кодовой единицей, индекс 5 для символов с двойной кодовой единицей, индекс 4 и ниже для последующих длинных символов ) - person Margaret Bloom; 21.12.2016
comment
Согласен и спасибо. Я отметил ваш как решение. - person Frank C.; 21.12.2016
comment
Вместо neg al + add al, 7 + setz al почему бы не просто cmp al, 14 + setz al? - person Cody Gray; 21.12.2016
comment
@CodyGray Мне нужно вычислить функцию f(i) так, чтобы: f(7) -> 1, f(5) -> 2, f(4) -> 3, f(3) -> 4. Так что я делаю 7-i == 0 ? 1 : 7-i. Я не следую предложенному вами решению. Кстати, спасибо за редактирование! - person Margaret Bloom; 21.12.2016
comment
Тогда вы не можете использовать setz, потому что это безоговорочно забивает al. Он будет либо установлен на 0, либо на 1, в зависимости от ZF. - person Cody Gray; 21.12.2016
comment
@CodyGray Черт... ты прав! Я исправляю это. Спасибо еще раз! - person Margaret Bloom; 21.12.2016
comment
Я думаю, вы можете xor al, 7 сделать al = 7-al. gcc и clang используют этот трюк с дополнением до 2 при компиляции функции с ведущими нулями с BSR (т. е. когда LZCNT недоступен). godbolt.org/g/lDfjNL. - person Peter Cordes; 23.12.2016
comment
Вы можете избежать уничтожения eax, используя bmi2 rorx edx, eax, 8, чтобы скопировать и повернуть младшие биты в начало 32-битный регистр для настройки LZCNT (который недоступен с 8-битным размером операнда). У меня возникла эта идея, когда я пытался закодировать ее на C, где функции с начальным нулевым счетом не имеют маленьких размеров, поэтому в итоге я использовал __builtin_clz(~(foo<<24)). gcc и clang просто выполняют сдвиг вместо использования 8-битного размера операнда. - person Peter Cordes; 23.12.2016
comment
Спасибо @PeterCordes, какое умное использование xor! - person Margaret Bloom; 23.12.2016

Если вы настаиваете на обратном порядке байтов (по какой-то странной причине), вы все равно можете просто отсканировать первый бит, равный 1, разделить на 8 и +1, чтобы получить количество байтов.

GetReversedShiftedUtf8BytesCount:
    ; eax = UTF8 code in reversed order, by from LSB
    ; 'É' (c3 89) => eax = 0x0000c389
    bsr ecx,eax
    cmovz ecx,eax   ; needed only for eax = 0
      ; ^ if eax is never 0 on input, this "cmovz" can be removed
    shr ecx,3
    inc ecx
    ret

Когда вы помещаете первый байт символа в MSB, он будет производить количество битов 15, 23 или 31 для многобайтовых символов, для 7b ASCII что-либо от 0 до 6 будет производиться bsr. «div 8» исправит их все, в любом случае, ему все равно.

Эта процедура должна фактически работать также с действительными нормальными кодами UTF8.

Для неверного кода UTF8, заканчивающегося нулевым байтом, будет возвращено неправильное количество байтов (без нулевых).


Конечно, как и всегда, также возможно решение LUT:

    movzx  ecx,al
    shr    ecx,3
    movzx  ecx,byte [utf8lengthLUT + ecx]  ; +rcx for 64b
    ; ecx = number of bytes or 0 for invalid leading byte value
    ...

utf8lengthLUT:                     ; 32B look-up table for upper 5b of 1st byte
    db     1, 1, 1, 1, 1, 1, 1, 1  ; 00000 - 00111 ; single byte
    db     1, 1, 1, 1, 1, 1, 1, 1  ; 01000 - 01111 ; single byte
    db     0, 0, 0, 0, 0, 0, 0, 0  ; 10000 - 10111 ; not valid leading byte
    db     2, 2, 2, 2              ; 11000 - 11011 ; two bytes code point
    db     3, 3                    ; 11100 - 11101 ; three bytes code point
    db     4                       ; 11110         ; four bytes code point
    db     0                       ; 11111         ; not valid leading byte

Я не отлаживал его, просто пытался перевести с помощью nasm для проверки синтаксиса. И я не профилировал его, конечно. :) Глядя на краткость этого варианта bsr, я сомневаюсь, что он будет серьезно быстрее даже на процессорах, где bsr вредит.

Но этот обрабатывает недопустимые коды операций UTF8 по-другому, вместо того, чтобы обнаруживать ненулевой MSB и возвращать его число + 1 (не чувствительно к содержимому начального байта), он правильно декодирует информацию о начальном байте и возвращает 0, когда начальные биты неверны. Но правильные начальные биты с неправильным 2-м+ байтом (например, c3 00) все равно будут возвращать 2, тогда как первый вариант в таком случае возвращает 1.

(можно использовать только 16-битную таблицу LUT, если вас не волнует недопустимая информация о начальных 11111 байтах, и вы примете это как 4-байтовую кодовую точку)


Кстати, есть некоторые библиотеки i18n (с открытым исходным кодом), выполняющие все эти действия, такие как проверка входных данных utf8, исправление неверных, подсчет символов и т. д. Некоторые из них существуют уже более десяти лет... и все еще получают ошибки. отчеты и исправления. Что является своего рода тонким намеком на то, как сложно правильно написать этот материал (не подвергая приложение какой-либо уязвимости входных данных). :)

(плюс учитывая, сколько (исправлений) правок получили эти два ответа... :))

И еще один оффтопный совет: если вы когда-нибудь попытаетесь написать что-то на PHP, то должны обрабатывать входные данные UTF8 (которые не из доверенного источника, но даже из доверенного источника), особенно если эти входные данные из ответа GET/POST. .. только не по своему усмотрению. Никогда. Получите некоторую основу для этого. :)

person Ped7g    schedule 21.12.2016
comment
Заказ был ошибкой с моей стороны. Предположим, что eax = 0x000089c3 - person Frank C.; 21.12.2016
comment
@ФранкК. Тогда я полагаю, что решение Маргарет тоже будет работать, но также и это должно быть для действительных кодов UTF-8, потому что MSB по-прежнему не может быть равен нулю, поэтому какой-то бит будет установлен в MSB, div 8 все равно получит правильный результат [0,1,2,3]. ... Этот может выйти из строя из-за недопустимых кодов UTF8 (с нулевым байтом внутри (в конце)). В таком случае убедитесь, что ваш код не может быть взломан. - person Ped7g; 21.12.2016
comment
Хороший (+1) балл за эксплойт. Я добавлю больше строгости к приему. Благодарю вас! - person Frank C.; 21.12.2016
comment
@FrankC.: Кстати, это, конечно, работает только потому, что вы уже проанализировали символ до нужной длины. Поэтому, если вы действительно заинтересованы в производительности, у первого парсера уже есть длина. Просто возьмите это оттуда. Это имело бы смысл, если бы у вас был в eax символ UTF-8 + неопределенный беспорядок после него, так что это был бы первый случай обработки символа. Но тогда код должен быть другим. Или Маргарет (которая не определена для FF... оооочень... еще есть над чем поработать, если вам нужно быть пуленепробиваемым :)). - person Ped7g; 21.12.2016
comment
У меня нет роскоши изменять метаданные структуры/объекта char, чтобы включить длину, поэтому у меня есть синтаксический анализ, происходящий в одном месте, и использование, которое может произойти «в какой-то более поздний момент» в будущем (например, печать, и т.д.) - person Frank C.; 21.12.2016
comment
хм... Данные utf-8 могут быть потоком байтов (как в текстовом файле utf-8). Итак, char [] кодирует все это, включая длину кодов и даже недействительные.. :) и с минимальными затратами. Анализ этого потока на отдельные символы непосредственно перед работой с отдельными символами обычно является достаточно хорошим, но если вы действительно интенсивно работаете с отдельными символами, вы можете сохранить их отдельно от потока, тратя больше памяти и повторяя их несколько раз. . Но исходная кодировка utf-8 на самом деле очень удобна для большинства задач, так что вы, вероятно, делаете что-то особенное. :) - person Ped7g; 21.12.2016
comment
Вообще говоря, это олдскульный брутфорс, у которого в MSB байте установлен какой-то бит. Решение Маргарет фактически анализирует данные управляющего байта UTF8. Поэтому я думаю, что оба ответа дают некоторое представление и могут быть предпочтительными в разных ситуациях. Ура. :) - person Ped7g; 21.12.2016
comment
@ Ped7g Ped7g Понятно, мне нравится не уничтожать eax и использовать ecx так, как вы закодировали. - person Frank C.; 21.12.2016