Intel SSE: почему `_mm_extract_ps` возвращает` int` вместо `float`? - PullRequest
9 голосов
/ 03 апреля 2011

Почему _mm_extract_ps возвращает int вместо float?

Как правильно читать один float из регистра XMM в C?

Или, скорее, другой способ задать это: Что противоположно инструкции _mm_set_ps?

Ответы [ 4 ]

18 голосов
/ 23 июня 2013

Ни один из ответов, по-видимому, не отвечает на вопрос, почему возвращает int.

Причина в том, что инструкция extractps фактически копирует компонент векторав общий реестр.Возвращение целого числа кажется довольно глупым, но это то, что происходит на самом деле - необработанное значение с плавающей запятой заканчивается в общем регистре (который содержит целые числа).

Если ваш компилятор настроен на генерацию SSE для всехОперации с плавающей запятой, тогда самым близким к «извлечению» значения в регистр было бы перетасовать значение в младший компонент вектора, а затем привести его к скалярному плавающему значению.Это должно привести к тому, что этот компонент вектора останется в регистре SSE:

/* returns the second component of the vector */
float foo(__m128 b)
{
    return _mm_cvtss_f32(_mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 2)));
}

Свойство _mm_cvtss_f32 свободно, оно не генерирует инструкции, оно только заставляет компилятор переосмыслить регистр xmm как float, поэтому он может быть возвращен как таковой.

_mm_shuffle_ps получает желаемое значение в низший компонент.Макрос _MM_SHUFFLE генерирует непосредственный операнд для результирующей инструкции shufps.

* В примере 2 получает значение float из бита 95:64 регистра 127: 0 (3-й 32-битный компонентс начала, в порядке памяти) и помещает его в компонент регистра 31: 0 (начало, в порядке памяти).

Полученный сгенерированный код, скорее всего, будет возвращать значение естественным образом в регистре,как и любое другое возвращаемое значение с плавающей запятой, без неэффективной записи в память и последующего чтения.

Если вы генерируете код, использующий FPU x87 для плавающей запятой (для обычного кода C, который не является SSE)оптимизировано), это, вероятно, приведет к генерации неэффективного кода - компилятор, вероятно, сохранит компонент вектора SSE, а затем использует fld для считывания его обратно в стек регистров x87.В целом 64-битные платформы не используют x87 (они используют SSE для всех чисел с плавающей запятой, в основном скалярных инструкций, если компилятор не векторизует).

Я должен добавить, что я всегда использую C ++, поэтому я неуверен, что более эффективно передавать __m128 по значению или по указателю в C. В C ++ я бы использовал const __m128 &, и этот тип кода был бы в заголовке, поэтому компилятор может быть встроенным.

7 голосов
/ 16 декабря 2016

Запутанно, 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.

1 голос
/ 03 апреля 2011

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

Обратите внимание, что в их примере значение 0xc0a40000 эквивалентно -5,125 (a.m128_f32 [1]).

Обновление: я настоятельно рекомендую вместо @ doug65536 и @PeterCordes (ниже) ответы, которые, по-видимому, генерируют неэффективный код на многих компиляторах.

1 голос
/ 03 апреля 2011

Попробуйте _mm_storeu_ps, или любой из вариантов операций хранилища SSE .

...