Почему MSVC выбрасывает бесполезный MOVSX перед выполнением этого теста бит?
Компиляция следующего кода в MSVC 2013, сборка с 64-разрядной версией, /O2
Оптимизация:
while (*s == ' ' || *s == ',' || *s == '\r' || *s == '\n') {
++s;
}
Я получаю следующий код, который имеет очень крутую оптимизацию, используя 64-битный регистр в качестве таблицы поиска с инструкцией bt
(бит тест).
mov rcx, 17596481020928 ; 0000100100002400H
npad 5
[email protected]:
movzx eax, BYTE PTR [rsi]
cmp al, 44 ; 0000002cH
ja SHORT [email protected]
movsx rax, al
bt rcx, rax
jae SHORT [email protected]
inc rsi
jmp SHORT [email protected]
[email protected]:
; code after loop...
Но мой вопрос: какова цель movsx rax, al
после первой ветки?
Сначала мы загружаем байт из строки в rax
и нуль-расширяем его:
movzx eax, BYTE PTR [rsi]
Затем пара cmp
/ja
выполняет сравнение без знака между al
и 44
и разветвляется вперед, если al
больше.
Итак, теперь мы знаем 0 <= al <= 44
в неподписанных числах. Поэтому невозможно установить самый старший бит al
!
Тем не менее, следующая инструкция movsx rax, al
. Это расширенный шаг. Но так как:
-
al
- младший байт rax
- мы уже знаем, что остальные 7 байтов
rax
обнуляются
- мы просто доказали, что
al
старший бит не может быть установлен
этот movsx
должен быть не-op.
Почему MSVC это делает? Я предполагаю, что это не для заполнения, так как в этом случае другой npad
сделает смысл более понятным. Является ли это сбросом данных или что-то?
(Кстати, эта оптимизация bt
действительно делает меня счастливой. Интересные факты: она работает в 0,6 раза от времени 4 cmp
/je
, которые вы могли бы ожидать, это способ быстрее, чем strspn
или std::string::find_first_not_of
, и это происходит только в 64-битных сборках, даже если интересующие символы имеют значения ниже 32.)
Ответы
Ответ 1
Вы наверняка осознаете, что эта оптимизация была произведена с помощью очень специфического кода в оптимизаторе, который искал шаблон. Просто генерирует бит-маска. Да, хороший трюк.
Здесь есть два основных случая. Первый - более универсальный, где (charmax - charmin <= 64), но charmax >= 64. Оптимизатор должен генерировать другой код из того, что вы видели, ему нужно вычесть charmin. В этой версии нет инструкции MOVSX. Вы можете увидеть его, заменив *s == ' '
на *s == 'A'
.
Тогда есть специальный случай, который вы протестировали, все коды символов для тестирования, 64. Программист Microsoft справился с этим в своем коде, поэтому он не создавал глупую инструкцию SUB EAX, 0. Но не заметил, что генерация MOVSX не нужна. Разумеется, пропустили только проверку оптимального кода в общем случае. И вызов общей функции в коде, так легко пропустить, обратите внимание на то, как команда изменяется на MOVZX при компиляции с помощью /J. В противном случае, легко считаться необходимым, нет инструкции BT, которая берет 8-битный регистр как 2-й операнд, так что загрузка регистров AL сама по себе недостаточна.
Может быть гипотетический оптимизатор пост-оптимизатора, который оптимизирует оптимизированный код, созданный оптимизатором. И решил сохранить MOVSX для улучшения суперскалярного исполнения. Я серьезно сомневаюсь, что он существует.
Ответ 2
Как уже упоминал Ханс Пассант, ваш код является частным случаем оптимизации. Я не смотрел в источники компилятора/оптимизатора, поэтому могу только сказать, что я ожидаю. Однако, вот мое объяснение этому.
Ваш код в C/С++:
while (*s == ' ' || *s == ',' || *s == '\r' || *s == '\n') {
++s;
}
эквивалентно:
while (*s == 32 || *s == 44 || *s == 13 || *s == 12) {
++s;
}
или
while (*s == 12 || *s == 13 || *s == 32 || *s == 44) {
++s;
}
Оптимизатор обнаруживает, что существует "if" с несколькими ( >= 3 раза) сравнениями одного и того же символа. Теперь оптимизатор генерирует как можно больше 64-битных шаблонов (до 4 шаблонов для 8 бит char, 64 * 4 = > все 256 возможных значений). Каждый из этих битовых шаблонов имеет смещение (= тестовое значение, которому соответствует первый бит в шаблоне), который необходимо вычесть из значения в тесте.
В вашем случае требуется только один 64-битный шаблон для значений от 12 до 44. Таким образом, ваш код преобразуется в какой-то общий код следующим образом:
#define ranged_bittest(pattern, value, min_val, max_val) \
(val >= min_val) && (val <= max_val) && BT_with_offset(pattern, val, min_val)
while ( !ranged_bittest(<BITPATTERN>, *s, 12, 44) ) {
++s;
}
Теперь какая-то другая оптимизация принимает ranged_bittest(<BITPATTERN>, *s, 12, 44);
, поскольку она обнаруживает bittest с начальным смещением 12 и шаблон, который может быть смещен влево, на 12 бит (поскольку верхние 12 бит шаблона равны нулю).
ranged_bittest(<BITPATTERN>, *s, 12, 44)
= > ranged_bittest(<BITPATTERN> << 12, *s, 0, 44)
Это развивается до:
while (!(val >= 0 && val <= 44 && BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0))) {
++s;
}
val >= 0 &&
оптимизируется (как предполагается, всегда верно), а "while" переводится в "do" -loop с разрывами; (что лучше для прогнозирования ветвлений в большинстве случаев), в результате чего:
do {
if (val > 44) break;
if (BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0)) break;
++s;
} while (true);
По какой-либо причине оптимизация останавливается здесь (например, поскольку существует ограничение на то, как часто дальнейшие оптимизации применяются к фрагменту кода по соображениям производительности).
Теперь в сборке строка
if (BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0)) break;`
переводится на что-то вроде:
MOV <reg64_A>, const_pattern
MOV <reg64_B>, value
SUB <reg64_B>, const_offset
BT <reg64_A>, <reg64_B>
Оптимизатор сборки уменьшает:
MOV <reg64_B>, value
SUB <reg64_B>, 0
просто
MOVSX <reg64_B>, value
в вашем специальном случае это:
MOVSX rax, al
Оптимизатор сборки (как-то) не имеет достаточной информации, чтобы полностью исключить расширение знака. Может быть, оптимизатор сборки довольно "тупой" (не может делать никаких оптимизаций логических выражений и т.д., Просто простых замен) или пока еще не активирован полный уровень оптимизации.
Поэтому он не может удалить movsx rax,al
, так как он не имеет никакой метаинформации о возможных значениях rax
или al
в этой точке кода.
Я НЕ ЗНАЮ, если это правда, но это мое лучшее предположение для этого случая...
Ответ 3
Что поразило меня больше всего, когда я впервые увидел этот код, так это то, что на самом деле он оптимизирован.
Да, это аккуратный трюк для использования 64-битного регистра для таблицы поиска, но...
- Зачем использовать
INC
в отличие от лучшего ADD,1
?
- Почему
CMP ...
JA ...
MOVSX ...
, когда мы знаем, что инструкция BT использует свой исходный операнд MOD 64, если его операнд назначения является регистром (так что есть 58 бит, которые не нужно учитывать)?
- Почему бы не воспользоваться тем фактом, что условные ветки, как прогнозируется, идут назад?
- Почему бы не уменьшить количество ветвей?
Я думаю, что истинный программист ассемблера написал бы это более как это:
mov rcx, 0FFFFEFFEFFFFDBFFh ;~0000100100002400h
sub rsi, 1
npad 1
[email protected]:
add rsi, 1
movzx eax, BYTE PTR [rsi] ;mov al,[rsi]
test al, 11000000b
setz bl
test bl, bl
bt rcx, rax
ja SHORT [email protected]
ja
прыгает, если (CF или ZF) = 0
- ZF = 1 для AL = [64,255] и не заботится о CF
- ZF = 0 для AL = [0,63] и CF = 0 для AL = {10,13,32,44}
Для всех ASCII в диапазоне [64,255] команда test al, 11000000b
дает ненулевой результат (ZF = 0). Поскольку combo setz bl
test bl, bl
затем используется для переключения ZF на 1, больше нет возможности для команды ja
продолжить цикл.
Напротив, для всех ASCII в диапазоне [0,63] ZF в конечном итоге будет 0, что позволяет ja
полностью интерпретировать CF, полученный из инструкции bt rcx, rax
.
Возможно, мы ожидаем от наших оптимизирующих компиляторов?