Почему GCC настаивает на том, чтобы не использовать регистры xmm для XOR?

# #c #gcc #x86 #compiler-optimization #sse

Вопрос:

Я тестирую часть математической библиотеки, над которой работаю, и обнаружил небольшую странность, глядя на сгенерированную сборку.

Игнорируя беспорядок кода шаблона, чтобы попасть сюда, код, который я тестирую, по сути, сводится к умножению двух поплавков, а затем фиксированию бита знака на некоторое значение.

Я также скомпилировал ( -O3 в обоих случаях) сокращенную версию кода:

 int main() {
    float a = *reinterpret_cast<float*>(0x20);
    float b = *reinterpret_cast<float*>(0x40);

    float tmp = a * b;
    *reinterpret_cast<int*>(amp;tmp) ^= 0x80000000;

    return tmp;
}
 

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

Из моего кода и примера GCC, по сути, генерирует следующее:

 mulss xmm0, xmm1
mov eax, 80000000
movd DWORD PTR SS:[rsp   2c], xmm0
add eax, DWORD PTR SS:[rsp   2c]
movd xmm2, eax
cvttss2si eax, xmm2
 

Глядя на это, я замечаю пару вещей:

  • GCC, похоже, предпочитает add вместо xor . В ответе на вопрос, который я нашел во время исследования, упоминалось, что an add должен быть немного быстрее из-за того, что последовательные инструкции не блокируются в конвейере, но я не понимаю, почему xor нужно ждать (коммутативный и ассоциативный, я что-то упускаю?).
  • Память используется как временная вместо регистра. Не должно быть никакой задержки между записью и чтением из-за пересылки хранилища, но в какой-то момент эту строку кэша придется записать в память, это просто кажется расточительным.
  • В соответствии с предыдущим пунктом GCC, похоже, настаивает на использовании другого регистра ( eax ) для выполнения XOR, хотя существуют инструкции, которые будут отлично работать с регистром XMM.

Из того, что я могу сказать, что-то вроде этого:

 mulss xmm0, xmm1
mov eax, 0x80000000
movd xmm1, eax
pxor xmm0, xmm1
cvttss2si eax, xmm0
 

было бы гораздо разумнее.

Это позволяет избежать ненужного доступа к памяти и не требует перемещения значения XMM обратно в регистр XMM для преобразования в целое число (я немного беспокоился, что GCC не использовал pxor , поскольку для этого требуется SSE2, но -msse2 ничего не изменил).

Я думал , что, возможно pxor , это будет значительно медленнее, чем add , но, согласно https://www.agner.org/optimize/instruction_tables.pdf, на Skylake, pxor x, x (по сравнению с add r, m ) имеет немного меньшую взаимную пропускную способность и генерирует меньше uops! (по крайней мере, не используется, и, честно говоря, разница в скорости, вероятно, связана только с включенным доступом к памяти)

Так в чем же дело? Почему GCC не создает что-то более близкое к моей пользовательской сборке? Это ошибка? Или GCC видит что — то, чего я не вижу.

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

1. лязг делает лучше, -march=native тоже немного помогает: godbolt.org/z/qMPcf8xTG

2. Как сравнивается сборка при выполнении tmp = -tmp; вместо этого?

3. @AlanBirtles: -march=native в Godbolt входит AVX-512. Это пока не очень реалистично для большинства вариантов использования. Godbolt даже предостерегает вас от использования -march=native , потому что это зависит от оборудования сервера AWS; используйте -march=skylake-avx512 или icelake-server если это то, на что вы действительно хотите посмотреть. Просто -march=haswell он выполняет отдельную vbroadcastss загрузку для подачи vxorps , по-прежнему не тратя впустую копирование в целое число и обратно.

4. Похоже, что GCC пропустил оптимизацию, а не недавнюю регрессию; например, присутствует в GCC4.9 godbolt.org/z/TEez77jnT . Даже с -fno-strict-aliasing тем, чтобы избежать неопределенного поведения в каламбуре типа с помощью приведения указателя вместо того же memcpy или (в GNU C ) объединения или C 20 std::bit_cast. И КСТАТИ, инструкции «3 операнда» исходят от включения AVX (через -march=native на достаточно новой машине). GCC всегда использует v версии инструкций XMM, когда включен AVX. SSE2 является базовым для x86-64, а SSE1 xorps -это то, что вы ожидаете от хорошего компилятора.

5. @the4naves: Это не «неиспользуемый», необходимо реализовать автономную версию функции , потому что это не static так, поэтому другие единицы компиляции могут вызвать ее. Вы получаете вывод asm компилятора для этого исходного файла C, а не связанного исполняемого файла, поэтому еще нет целой программы для оптимизации всей программы, чтобы рассмотреть возможность удаления определений функций, из которых в конечном итоге не вызываются main .