Подтвердить что ты не робот

Почему 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.)

4b9b3361

Ответ 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.

Возможно, мы ожидаем от наших оптимизирующих компиляторов?