# #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
. В ответе на вопрос, который я нашел во время исследования, упоминалось, что anadd
должен быть немного быстрее из-за того, что последовательные инструкции не блокируются в конвейере, но я не понимаю, почему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/qMPcf8xTG2. Как сравнивается сборка при выполнении
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, а SSE1xorps
-это то, что вы ожидаете от хорошего компилятора.5. @the4naves: Это не «неиспользуемый», необходимо реализовать автономную версию функции , потому что это не
static
так, поэтому другие единицы компиляции могут вызвать ее. Вы получаете вывод asm компилятора для этого исходного файла C, а не связанного исполняемого файла, поэтому еще нет целой программы для оптимизации всей программы, чтобы рассмотреть возможность удаления определений функций, из которых в конечном итоге не вызываютсяmain
.