Как слить скаляр в вектор без компилятора, теряющего инструкцию обнуления верхних элементов? Ограничение дизайна в 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. (См. x86 теги 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