Запутанно, int _mm_extract_ps()
не предназначен для получения скалярного float
элемента из вектора. Встроенная функция не раскрывает форму инструкции для назначения памяти (которая может быть полезна для этой цели).Это не единственный случай, когда встроенные функции не могут напрямую выразить все, для чего полезна инструкция.: (
gcc и clang знают, как работает инструкция asm, и будут использовать ее таким образом для вас при компиляции других перемешиваний; выведение типа _mm_extract_ps
на float
обычно приводит к ужасному asm от gcc (extractps eax, xmm0, 2
/ mov [mem], eax
).
Имя имеет смысл, если вы думаете о _mm_extract_ps
как об извлечении двоичного32-разрядного кода IEEE 754 из области FP процессора вцелочисленный домен (как скаляр C * int
), вместо манипулирования битовыми шаблонами FP с целочисленными векторными операциями. Согласно моему тестированию с gcc, clang и icc (см. ниже), это единственный "переносимый"сценарий использования, где _mm_extract_ps
компилируется в good asm во всех компиляторах . Все остальное - просто хак для конкретного компилятора, чтобы получить требуемый asm.
Соответствующая инструкция asm - EXTRACTPS r/m32, xmm, imm8
. Обратите внимание, что получателем может быть память или регистр integer , но не другой регистр XMM. Это FP эквивалент PEXTRD r/m32, xmm, imm8
(также в SSE4.1), где целое числоФорма регистрации-назначения более очевидна.EXTRACTPS не является противоположностью INSERTPS xmm1, xmm2/m32, imm8
.
Возможно, это сходство с PEXTRD делает внутреннюю реализацию проще без ущерба для варианта использования извлечения в память (для asm, а не для встроенных функций)или, может быть, разработчики SSE4.1 в Intel решили, что на самом деле это более полезно, чем как неразрушающее копирование и перемешивание в FP-домене (которого в x86 серьезно не хватает без AVX).Существуют инструкции вектора FP, которые имеют источник XMM и место назначения памяти или xmm, например MOVSS xmm2/m32, xmm
, поэтому такого рода инструкция не будет новой.Интересный факт: коды операций для PEXTRD и EXTRACTPS отличаются только последним битом.
В сборке скаляр float
является просто младшим элементом регистра XMM (или4 байта в памяти).Верхние элементы XMM даже не нужно обнулять, чтобы инструкции вроде ADDSS работали без каких-либо дополнительных исключений FP.В соглашениях о вызовах, которые передают / возвращают аргументы FP в регистрах XMM (например, все обычные ABI x86-64), float foo(float a)
должен предполагать, что верхние элементы XMM0 содержат мусор при входе, но могут оставлять мусор в старших элементах XMM0 привернуть.( Подробнее ).
Когда @doug указывает на , другие команды shuffle могут использоваться для получения элемента с плавающей точкой вектора в нижней части регистра xmm., Эта проблема уже была в основном решена в SSE1 / SSE2 , и кажется, что EXTRACTPS и INSERTPS не пытались ее решить для операндов регистров.
SSE4.1 INSERTPS xmm1, xmm2/m32, imm8
- один из лучших способов для компиляторов реализовать _mm_set_ss(function_arg)
, когда скалярное число с плавающей точкой уже находится в регистре, и они не могут / не оптимизируют обнуление верхних элементов.( Что чаще всего используется компиляторами, кроме clang ).В этом связанном вопросе также обсуждается неспособность встроенных функций раскрыть загруженные или сохраненные версии инструкций, таких как EXTRACTPS, INSERTPS и PMOVZX, которые имеют операнд памяти, меньший, чем 128b (таким образом, не требующий выравнивания даже без AVX).Может быть невозможно написать безопасный код, который компилируется так же эффективно, как то, что вы можете сделать в asm.
Без AVX с 3 операндами SHUFPS x86 не обеспечивает полностью эффективного и универсального способа копирования и перетасовки вектора FP, как это может делать целое число PSHUFD . SHUFPS - это другой зверь, если не используется на месте с src = dst.Сохранение оригинала требует MOVAPS, который стоит Uop и задержка на процессорах до IvyBridge, и всегда стоит размер кода.Использование PSHUFD между инструкциями FP стоит задержки (задержки обхода).(См. этот ответ с горизонтальной суммой для некоторых уловок, таких как использование SSE3 MOVSHDUP).
SSE4.1 INSERTPS может извлекать один элемент в отдельный регистр, но AFAIK он все еще имеет зависимость отпредыдущее значение пункта назначения, даже если заменены все исходные значения.Подобные ложные зависимости плохо влияют на выполнение не по порядку. xor-zeroing регистр в качестве места назначения для INSERTPS будет по-прежнему равен 2 мопам и будет иметь меньшую задержку, чем MOVAPS + SHUFPS на процессорах SSE4.1 без исключения mov для исключения MOVAPS с нулевой задержкой (только Penryn, Nehalem,Sandybridge. Также Silvermont, если вы используете маломощные процессоры).Размер кода немного хуже, хотя.
Использование _mm_extract_ps
, а затем набрать результат обратно до плавающего значения (как предложено в принятом в настоящее время ответе и егокомментарии) это плохая идея.Ваш код легко компилируется во что-то ужасное (например, EXTRACTPS в память, а затем загружается обратно в регистр XMM) в gcc или icc.Кажется, что Clang невосприимчив к поведению braindead и выполняет свою обычную shuffle-compiling с собственным выбором команд shuffle (включая правильное использование EXTRACTPS).
Я пробовал эти примеры с gcc5.4 -O3 -msse4.1 -mtune=haswell
, clang3.8.1 и icc17, в проводнике компилятора Godbolt .Я использовал режим C, а не C ++, но в GNU C ++ допускается штамповка типов на основе объединения в качестве расширения ISO C ++.Приведение указателей для типа «наказание» нарушает строгие псевдонимы в C99 и C ++, даже с расширениями GNU.
#include <immintrin.h>
// gcc:bad clang:good icc:good
void extr_unsafe_ptrcast(__m128 v, float *p) {
// violates strict aliasing
*(int*)p = _mm_extract_ps(v, 2);
}
gcc: # others extractps with a memory dest
extractps eax, xmm0, 2
mov DWORD PTR [rdi], eax
ret
// gcc:good clang:good icc:bad
void extr_pun(__m128 v, float *p) {
// union type punning is safe in C99 (and GNU C and GNU C++)
union floatpun { int i; float f; } fp;
fp.i = _mm_extract_ps(v, 2);
*p = fp.f; // compiles to an extractps straight to memory
}
icc:
vextractps eax, xmm0, 2
mov DWORD PTR [rdi], eax
ret
// gcc:good clang:good icc:horrible
void extr_gnu(__m128 v, float *p) {
// gcc uses extractps with a memory dest, icc does extr_store
*p = v[2];
}
gcc/clang:
extractps DWORD PTR [rdi], xmm0, 2
icc:
vmovups XMMWORD PTR [-24+rsp], xmm0
mov eax, DWORD PTR [-16+rsp] # reload from red-zone tmp buffer
mov DWORD PTR [rdi], eax
// gcc:good clang:good icc:poor
void extr_shuf(__m128 v, float *p) {
__m128 e2 = _mm_shuffle_ps(v,v, 2);
*p = _mm_cvtss_f32(e2); // gcc uses extractps
}
icc: (others: extractps right to memory)
vshufps xmm1, xmm0, xmm0, 2
vmovss DWORD PTR [rdi], xmm1
Если вы хотите получить конечный результат в регистре xmm, то компилятор должен оптимизироватьубери свои выдержки и сделай что-то совершенно другое.Gcc и clang оба успешно, но ICC нет.
// gcc:good clang:good icc:bad
float ret_pun(__m128 v) {
union floatpun { int i; float f; } fp;
fp.i = _mm_extract_ps(v, 2);
return fp.f;
}
gcc:
unpckhps xmm0, xmm0
clang:
shufpd xmm0, xmm0, 1
icc17:
vextractps DWORD PTR [-8+rsp], xmm0, 2
vmovss xmm0, DWORD PTR [-8+rsp]
Обратите внимание, что icc плохо работает и для extr_pun
, так что он не любит наложения типов на основе объединения для этого.
Безусловным победителем здесь является случайное перемешивание с помощью _mm_shuffle_ps(v,v, 2)
и использование _mm_cvtss_f32
. . Мы получили оптимальный код от каждого компилятора как для регистров, так и для адресатов памяти, кромедля ICC, который не смог использовать EXTRACTPS для случая с памятью.При использовании AVX отдельное хранилище SHUFPS + по-прежнему занимает всего 2 моп на процессорах Intel, только больший размер кода и требуется регистр tmp.Однако без AVX MOVAPS не стоило бы уничтожать исходный вектор: /
Согласно таблицам инструкций Agner Fog , все процессоры Intel, за исключением Nehalem, реализуют регистр-назначениеверсии как PEXTRD, так и EXTRACTPS с несколькими мопами: обычно просто мефф shuffle + моп MOVD для перемещения данных из векторного домена в gp-integer.EXTRACTPS для регистра Nehalem - это 1 моп для порта 5 с задержкой цикла 1 + 2 (1 + задержка обхода).
Я понятия не имею, почему им удалось реализовать EXTRACTPS как один моп, а не PEXTRD (которыйравен 2 мопам и работает с задержкой цикла 2 + 1).Nehalem MOVD равен 1 моп (и работает на любом порте ALU) с задержкой цикла 1 + 1.(Думаю, +1 для задержки обхода между vec-int и целочисленными регистрами общего назначения.)
Nehalem заботится о векторном FP и целочисленных доменах;Процессоры семейства SnB имеют меньшую (иногда нулевую) задержку обхода между доменами.
Версии памяти и файлов PEXTRD и EXTRACTPS на Nehalem равны 2 мопам.
В Broadwell и более поздних версиях EXTRACTPS и PEXTRD назначения памяти - 2 мопа, а в Sandybridge через Haswell EXTRACTPS - назначения памяти 3 мопа. Назначение памяти PEXTRD - 2 моп на всем, кроме Sandybridge, где это 3. Это кажется странным, и таблицы Агнера Фога иногда имеют ошибки, но это возможно. Micro-fusion не работает с некоторыми инструкциями на некоторых микроархитектурах.
Если бы любая инструкция оказалась чрезвычайно полезной для чего-то важного (например, внутри внутренних циклов), разработчики ЦП построили бы исполнительные модули, которые могли бы выполнять все это как один моп (или, возможно, 2 для памяти-dest). Но для этого может потребоваться больше битов во внутреннем формате UOP (что упрощено Sandybridge).
Интересный факт: _mm_extract_epi32(vec, 0)
компилируется (на большинстве компиляторов) в movd eax, xmm0
, что короче и быстрее, чем pextrd eax, xmm0, 0
.
Интересно, что они работают по-разному на Nehalem (который заботится о векторных FP и целочисленных доменах и появился вскоре после того, как SSE4.1 был представлен в Penryn (45-нм Core2)). EXTRACTPS с регистром назначения составляет 1 моп, с задержкой цикла 1 + 2 (+2 от задержки обхода между FP и целочисленной областью). PEXTRD равен 2 моп и работает с задержкой цикла 2 + 1.