Если вы используете AVX2, вы можете использовать PMOVZX для расширения ваших символов до 32-битных целых чисел в регистре 256b. Оттуда преобразование в плавающее может происходить на месте.
; rsi = new_image
VPMOVZXBD ymm0, [rsi] ; or SX to sign-extend (Byte to DWord)
VCVTDQ2PS ymm0, ymm0 ; convert to packed foat
Это хорошая стратегия, даже если вы хотите сделать это для нескольких векторов, но еще лучше может быть 128-битная широковещательная нагрузка для подачи vpmovzxbd ymm,xmm
и vpshufb ymm
(_mm256_shuffle_epi8
) для старших 64 бит, потому что Intel В процессорах семейства SnB нет микроплавких предохранителей vpmovzx ymm,mem
, только vpmovzx xmm,mem
. (https://agner.org/optimize/). Широковещательные нагрузки являются одиночными и не требуют порта ALU, выполняются исключительно в порте загрузки. Итак, это всего 3 мупа для bcast-load + vpmovzx + vpshufb.
(TODO: напишите внутреннюю версию этого. Это также позволяет обойти проблему пропущенных оптимизаций для _mm_loadl_epi64
-> _mm256_cvtepu8_epi32
.)
Конечно, для этого требуется вектор управления тасованием в другом регистре, так что оно того стоит, только если вы можете использовать его несколько раз.
vpshufb
можно использовать, потому что данные, необходимые для каждой дорожки, поступают из широковещательной передачи, а старший бит управления перемешиванием обнуляет соответствующий элемент.
Эта стратегия трансляции + перемешивания может быть хорошей на Ryzen; Агнер Туман не указывает на нем количество мапов для vpmovsx/zx ymm
.
Не выполняйте что-то вроде 128-битной или 256-битной загрузки, а затем перемешивайте ее, чтобы ввести дальнейшие vpmovzx
инструкции. Общая пропускная способность при перемешивании, вероятно, уже будет узким местом, потому что vpmovzx
- это перемешивание. Intel Haswell / Skylake (наиболее распространенные архивы AVX2) имеют перетасовку 1 на такт, но 2 нагрузки на такт. Использование дополнительных инструкций перемешивания вместо сворачивания отдельных операндов памяти в vpmovzxbd
ужасно. Только если вы сможете уменьшить общее количество мопов, как я предложил с помощью broadcast-load + vpmovzxbd + vpshufb, это будет победа.
Мой ответ на Масштабирование байтовых значений пикселей ( y = ax + b) с SSE2 (как числа с плавающей запятой)? может иметь значение для обратного преобразования в uint8_t
. Последующая часть «pack back-to-bytes» полусложна, если делать это с AVX2 packssdw/packuswb
, потому что они работают в полосе движения, в отличие от vpmovzx
.
Только с AVX1, а не с AVX2, вам следует сделать:
VPMOVZXBD xmm0, [rsi]
VPMOVZXBD xmm1, [rsi+4]
VINSERTF128 ymm0, ymm0, xmm1, 1 ; put the 2nd load of data into the high128 of ymm0
VCVTDQ2PS ymm0, ymm0 ; convert to packed float. Yes, works without AVX2
Конечно, вам никогда не понадобится массив чисел с плавающей запятой, только __m256
векторов.
GCC / MSVC пропустили оптимизацию для VPMOVZXBD ymm,[mem]
со встроенными функциями
GCC и MSVC плохо складывают _mm_loadl_epi64
в операнд памяти для vpmovzx*
. (Но по крайней мере там есть внутренняя нагрузка правильной ширины, в отличие от pmovzxbq xmm, word [mem]
.)
Мы получаем vmovq
загрузку, а затем отдельный vpmovzx
с входом XMM. (С ICC и clang3.6 + мы получаем безопасный + оптимальный код от использования _mm_loadl_epi64
, как от gcc9 +)
Но gcc8.3 и более ранние версии могут свернуть _mm_loadu_si128
16-байтовую внутреннюю загрузку в 8-байтовый операнд памяти. Это дает оптимальный asm на -O3
в GCC, но небезопасно на -O0
, где он компилируется в фактическую vmovdqu
загрузку, которая затрагивает больше данных, которые мы фактически загружаем, и может уйти с конца страницы.
Из-за этого ответа было отправлено две ошибки gcc:
Нет никакого внутреннего принципа использовать SSE4.1 pmovsx
/ pmovzx
в качестве загрузки, только с __m128i
исходным операндом. Но инструкции asm считывают только тот объем данных, который они фактически используют, а не 16-байтовый __m128i
операнд источника памяти. В отличие от punpck*
, вы можете использовать это на последних 8B страницы без ошибок. (И на невыровненных адресах даже с версией, отличной от AVX).
Итак, вот злое решение, которое я придумал. Не используйте это, #ifdef __OPTIMIZE__
Плохо, что позволяет создавать ошибки, которые возникают только в отладочной сборке или только в оптимизированной сборке!
#if !defined(__OPTIMIZE__)
// Making your code compile differently with/without optimization is a TERRIBLE idea
// great way to create Heisenbugs that disappear when you try to debug them.
// Even if you *plan* to always use -Og for debugging, instead of -O0, this is still evil
#define USE_MOVQ
#endif
__m256 load_bytes_to_m256(uint8_t *p)
{
#ifdef USE_MOVQ // compiles to an actual movq then movzx ymm, xmm with gcc8.3 -O3
__m128i small_load = _mm_loadl_epi64( (const __m128i*)p);
#else // USE_LOADU // compiles to a 128b load with gcc -O0, potentially segfaulting
__m128i small_load = _mm_loadu_si128( (const __m128i*)p );
#endif
__m256i intvec = _mm256_cvtepu8_epi32( small_load );
//__m256i intvec = _mm256_cvtepu8_epi32( *(__m128i*)p ); // compiles to an aligned load with -O0
return _mm256_cvtepi32_ps(intvec);
}
Глупости vmovq
- это то, чего мы хотим избежать. Если вы позволите ему использовать небезопасную loadu_si128
версию, из него получится хороший оптимизированный код.
load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
GCC9, clang и ICC испускают:
Написание версии только для AVX1 с встроенными функциями оставлено как неприятное занятие для читателя. Вы просили «инструкции», а не «внутренние компоненты», и это то место, где есть пробелы во встроенных функциях. Необходимость использовать _mm_cvtsi64_si128
, чтобы избежать потенциальной загрузки с адресов вне границ, глупо, ИМО. Я хочу иметь возможность думать о встроенных функциях в терминах инструкций, которым они сопоставляются, а встроенные функции загрузки / хранения информируют компилятор о гарантиях выравнивания или их отсутствии. Необходимость использовать внутреннюю функцию для инструкции, которую я не хочу, довольно глупо.
load_bytes_to_m256(unsigned char*):
vpmovzxbd ymm0, qword ptr [rdi] # ymm0 = mem[0],zero,zero,zero,mem[1],zero,zero,zero,mem[2],zero,zero,zero,mem[3],zero,zero,zero,mem[4],zero,zero,zero,mem[5],zero,zero,zero,mem[6],zero,zero,zero,mem[7],zero,zero,zero
vcvtdq2ps ymm0, ymm0
ret
Также обратите внимание, что если вы просматриваете руководство Intel insn ref, для movq есть две отдельные записи:
movd / movq, версия, которая может иметь целочисленный регистр в качестве операнда src / dest (66 REX.W 0F 6E
(или VEX.128.66.0F.W1 6E
) для (V) MOVQ xmm, r / m64). Здесь вы найдете встроенную функцию, которая может принимать 64-битное целое число, _mm_cvtsi64_si128
. (Некоторые компиляторы не определяют его в 32-битном режиме.)
movq: версия, которая может иметь два регистра xmm в качестве операндов. Это расширение инструкции MMXreg -> MMXreg, которая также может загружать / сохранять, как MOVDQU. Его код операции F3 0F 7E
(VEX.128.F3.0F.WIG 7E
) для MOVQ xmm, xmm/m64)
.
Руководство asm ISA ref перечисляет только внутреннюю m128i _mm_mov_epi64(__m128i a)
для обнуления старших 64b вектора при его копировании. Но руководство по встроенным функциям перечисляет _mm_loadl_epi64(__m128i const* mem_addr)
, который имеет дурацкий прототип (указатель на 16-байтовый __m128i
тип, когда он действительно загружает только 8 байтов). Он доступен на всех 4 основных компиляторах x86 и должен быть безопасным. Обратите внимание, что __m128i*
просто передается этому непрозрачному встроенному объекту, не фактически разыменованный.
Также указан более разумный _mm_loadu_si64 (void const* mem_addr)
, но в gcc его нет.
Загрузка SSE / AVX movq (_mm_cvtsi64_si128) не сворачивается в pmovzx (< strong> исправлено для gcc9, но исправление нарушает свертывание нагрузки для 128-битной загрузки, поэтому обходной прием для старого GCC ухудшает работу gcc9.)
person
Peter Cordes
schedule
15.12.2015