Как слить скаляр в вектор без компилятора, теряющего инструкцию обнуления верхних элементов? Ограничение дизайна в Intel intrinsics?

У меня нет конкретного случая использования; Я спрашиваю, действительно ли это недостаток дизайна/ограничение в Intel intrinsics или если я просто что-то пропустил.

Если вы хотите комбинировать скалярный float с существующим вектором, похоже, нет способа сделать это без высокоэлементного обнуления или трансляции скаляра в вектор, используя встроенные функции Intel. Я не исследовал родные векторные расширения GNU C и связанные с ними встроенные функции.

Это было бы не так уж плохо, если бы дополнительный встроенный оптимизирован, но он не с gcc (5.4 или 6.2). Также нет хорошего способа использовать pmovzx или insertps в качестве загрузок, по той причине, что их intrinsics принимают только векторные args. (И gcc не складывает скалярную > векторную нагрузку в инструкцию asm.)

__m128 replace_lower_two_elements(__m128 v, float x) {
  __m128 xv = _mm_set_ss(x);        // WANTED: something else for this step, some compilers actually compile this to a separate insn
  return _mm_shuffle_ps(v, xv, 0);  // lower 2 elements are both x, and the garbage is gone
}

gcc 5.3 -march = nehalem -O3, чтобы включить SSE4.1 и настроить для этого процессора Intel: (Это еще хуже без SSE4.1, несколько инструкций для ноль верхних элементов).

    insertps  xmm1, xmm1, 0xe    # pointless zeroing of upper elements.  shufps only reads the low element of xmm1
    shufps    xmm0, xmm1, 0      # The function *should* just compile to this.
    ret

TL: DR: остальная часть этого вопроса просто спрашивает, действительно ли вы можете сделать это эффективно, а если нет, то почему.


clang shuffle-optimizer получает это право и не тратит инструкции на обнуление высоких элементов (_mm_set_ss(x)) или дублирует скаляр в них (_mm_set1_ps(x)). Вместо того, чтобы писать что-то, что компилятор должен оптимизировать, не должно быть способа написать его "эффективно" на C в первую очередь? Даже очень недавний gcc не оптимизирует его, так что это реальная (но незначительная) проблема.


Это было бы возможно, если бы был скаляр- > 128b эквивалент __m256 _mm256_castps128_ps256 (__m128 a). то есть создать __m128 с мусором undefined в верхних элементах, а float в нижнем элементе, скомпилировать в нулевые инструкции asm, если скалярный float/double уже был в регистре xmm.

Ни одна из следующих функций не существует, но они должны.

  • скаляр → __ m128 эквивалент _mm256_castps128_ps256, как описано выше. Наиболее общее решение для случая скалярного уже в регистре.
  • __m128 _mm_move_ss_scalar (__m128 a, float s): замените нижний элемент вектора a на скалярный s. Это на самом деле не обязательно, если имеется скаляр общего назначения → __ m128 (предыдущая маркерная точка). (Регистрационная форма movss объединяет, в отличие от формы загрузки, нули и в отличие от movd, который в обоих случаях нулевает верхние элементы. Чтобы скопировать регистр, содержащий скалярный float без ложных зависимостей, используйте movaps).
  • __m128i _mm_loadzxbd (const uint8_t *four_bytes) и другие размеры PMOVZX/PMOVSX: AFAICT, там нет хороший безопасный способ использования инфраструктуры PMOVZX в качестве нагрузки, поскольку неудобный безопасный способ не оптимизируется с помощью gcc.
  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8). INSERTPS ведет себя по-разному в качестве нагрузки: верхние 2 бита imm8 игнорируются и всегда принимают скаляр по эффективному адресу ( вместо элемента из вектора в памяти). Это позволяет работать с адресами, не выравниваемыми по 16B, и работать даже без сбоев, если float прямо перед неотображаемой страницей.

    Как и в случае с PMOVZX, gcc не сбрасывает верхний элемент-обнуление _mm_load_ss() в операнд памяти для INSERTPS. (Заметим, что если верхние 2 бита imm8 не равны нулю, то _mm_insert_ps(xmm0, _mm_load_ss(), imm8) может скомпилироваться с insertps xmm0,xmm0,foo с другим imm8, который имеет нулевые элементы в vec as-if, если элемент src фактически был нолем, созданным MOVSS из памяти. В этом случае Clang фактически использует XORPS/BLENDPS)


Существуют ли какие-либо жизнеспособные обходные способы для эмуляции любого из безопасных (не разбивайте на -O0, например, загружая 16B, которые могут касаться следующей страницы и segfault), и эффективны (нет потраченные впустую инструкции при -O3 с текущим gcc и clang, по крайней мере, предпочтительно также с другими крупными компиляторами)? Предпочтительно также читаемым образом, но при необходимости его можно разместить за встроенной функцией обертки, например __m128 float_to_vec(float a){ something(a); }.

Есть ли веские основания для того, чтобы Intel не вводила подобные функции? Они могли бы добавить float → __ m128 с верхними элементами undefined в то же время, что и добавление _mm256_castps128_ps256. Является ли это вопросом внутренних компонентов компилятора, затрудняющим его реализацию? Возможно, в первую очередь, внутренние подразделения ICC?


Основные соглашения вызова на x86-64 (SysV или MS __vectorcall) принимают первый аргумент FP в xmm0 и возвращают скалярные аргументы FP в xmm0 с верхними элементами undefined. (См. теги wiki для документов ABI). Это означает, что компилятор нередко имеет скалярный float/double в регистре с неизвестными верхними элементами. Это будет редко встречается в векторизованном внутреннем цикле, поэтому я думаю, что избежать этих бесполезных инструкций в основном просто сохранит немного размера кода.

Случай с pmovzx более серьезен: это то, что вы можете использовать во внутреннем цикле (например, для LUT из масок Shuffle VPERMD, сохраняя коэффициент 4 в кеш-памяти и сохраняя каждый индекс, заполненный до 32 бит в памяти).


Проблема pmovzx-as-a-load меня беспокоила какое-то время, и оригинальная версия этого вопроса заставляла меня задуматься о связанной с этим проблеме используя скалярное поплавок в регистре xmm. Вероятнее всего, для pmovzx в качестве нагрузки используется больше, чем для скалярного → __ m128.

Ответы

Ответ 1

Это применимо к GNU C inline asm, но это уродливо и поражает многие оптимизации, включая постоянное распространение (https://gcc.gnu.org/wiki/DontUseInlineAsm). Это не будет принятый ответ. Я добавляю это как ответ вместо части вопроса, поэтому вопрос остается коротким не огромен.

// don't use this: defeating optimizations is probably worse than an extra instruction
#ifdef __GNUC__
__m128 float_to_vec_inlineasm(float x) {
  __m128 retval;
  asm ("" : "=x"(retval) : "0"(x));   // matching constraint: provide x in the same xmm reg as retval
  return retval;
}
#endif

Это компилируется по отдельному ret, если требуется, и будет встроен, чтобы вы shufps скаляр в вектор:

gcc5.3
float_to_vec_and_shuffle_asm(float __vector(4), float):
    shufps  xmm0, xmm1, 0       # tmp93, xv,
    ret

Смотрите этот код в проводник компилятора Godbolt.

Это, очевидно, тривиально в чистом ассемблере, где вам не нужно сражаться с компилятором, чтобы он не выдавал инструкции, которые вам не нужны или нужны.


Я не нашел реального способа написать __m128 float_to_vec(float a){ something(a); }, который компилируется только в инструкцию ret. Попытка double с использованием _mm_undefined_pd() и _mm_move_sd() действительно делает худший код с gcc (см. Ссылку Godbolt выше). Ничего из существующей поддержки float → __ m128 intrinsics.


Вне темы: фактические стратегии _mm_set_ss() code-gen. Когда вы пишете код, который имеет нулевые верхние элементы, компиляторы выбирают из интересного диапазона стратегий. Некоторые хорошие, некоторые странные. Стратегии также различаются между double и float на одном и том же компиляторе (gcc или clang), как вы можете видеть на ссылке Godbolt выше.

Один пример: __m128 float_to_vec(float x){ return _mm_set_ss(x); } компилируется в:

    # gcc5.3 -march=core2
    movd    eax, xmm0      # movd xmm0,xmm0 would work; IDK why gcc doesn't do that
    movd    xmm0, eax
    ret

    # gcc5.3 -march=nehalem
    insertps        xmm0, xmm0, 0xe
    ret

    # clang3.8 -march=nehalem
    xorps   xmm1, xmm1
    blendps xmm0, xmm1, 14          # xmm0 = xmm0[0],xmm1[1,2,3]
    ret