Ответ 1
Если вы используете 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
для старших 64 бит, потому что процессоры семейства Intel SnB не t micro-fuse a vpmovzx ymm,mem
, только только vpmovzx xmm,mem
. (https://agner.org/optimize/). Широковещательные загрузки выполняются по одному каналу без порта ALU, работающего исключительно в порту загрузки. Так что это всего 3 мопа для bcast-load + vpmovzx + vpshufb.
(TODO: напишите встроенную версию этого. Это также обходит проблему пропущенных оптимизаций для _mm_loadl_epi64
→ _mm256_cvtepu8_epi32
.)
Конечно, для этого требуется вектор управления тасованием в другом регистре, поэтому он того стоит, если вы можете использовать его несколько раз.
vpshufb
для использования, потому что данные, необходимые для каждой vpshufb
из vpshufb
, и старший бит элемента управления перемешиванием обнуляет соответствующий элемент.
Эта стратегия широковещания и трансляции может быть полезной для Райзена; Agner Fog не перечисляет количество vpmovsx/zx ymm
для vpmovsx/zx ymm
на нем.
Не делать что - то вроде 128-битной или 256-битной нагрузки, а затем перетасовать, что кормить дальнейшее vpmovzx
инструкцией. Общая пропускная способность shuffle, вероятно, уже будет узким местом, потому что vpmovzx
- shuffle. Intel Haswell/Skylake (наиболее распространенные AVX2 uarches) имеют тасования по 1 такту, но по 2 такта. Использование дополнительных команд shuffle вместо складывания отдельных операндов памяти в vpmovzxbd
ужасно. Только если вы можете уменьшить общее количество мопов, как я предложил для широковещательной загрузки + vpmovzxbd + vpshufb, это победа.
Мой ответ о масштабировании значений байтовых пикселей (y = ax + b) с SSE2 (как плавающие)? может иметь значение для преобразования обратно в uint8_t
. Последующая часть pack-back-to-bytes будет packssdw/packuswb
хитрой, если делать это с AVX2 packssdw/packuswb
, потому что они работают в vpmovzx
, в отличие от 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]
со встроенными 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-байтовую загрузку _mm_loadu_si128
в 8-байтовый операнд памяти. Это дает оптимальное asm в -O3
в GCC, но небезопасно в -O0
где он компилируется с фактической vmovdqu
которая затрагивает больше данных, которые мы фактически загружаем, и может выходить за пределы страницы.
Из-за этого ответа отправлено две ошибки gcc:
- Загрузка SSE/AVX movq (_mm_cvtsi64_si128) не складывается в pmovzx (исправлено для gcc9, но исправление нарушает складывание нагрузки для 128-битной загрузки, поэтому обходной путь для старого GCC делает gcc9 хуже).
- Нет встроенного для x86
MOVQ m64, %xmm
в 32-MOVQ m64, %xmm
режиме. (TODO: сообщите об этом также для clang/LLVM?)
Не существует встроенного использования SSE4.1 pmovsx
/pmovzx
в качестве нагрузки, только с исходным операндом __m128i
. Но инструкции asm читают только тот объем данных, который они фактически используют, а не 16-байтовый __m128i
источника памяти __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);
}
load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
Глупый vmovq
- это то, чего мы хотим избежать. Если вы позволите ему использовать небезопасную версию loadu_si128
, он сделает хороший оптимизированный код.
GCC9, clang и ICC выделяют:
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
Написание версии, предназначенной только для AVX1, со встроенными функциями остается для читателя невеселым занятием. Вы просили "инструкции", а не "внутренности", и это единственное место, где есть пробел в внутренностях. IMO - глупо использовать _mm_cvtsi64_si128
чтобы избежать потенциальной загрузки из-за границы адресов. Я хочу иметь возможность рассматривать встроенные функции в терминах инструкций, которые они отображают, а встроенные функции загрузки/хранения информируют компилятор о гарантиях выравнивания или об их отсутствии. Необходимость использовать встроенное для инструкции, которую я не хочу, довольно глупа.
Также обратите внимание, что если вы ищете в руководстве 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)
для обнуленияm128i _mm_mov_epi64(__m128i a)
64b вектора при копировании. Но в руководстве по встроенным_mm_loadl_epi64(__m128i const* mem_addr)
есть список_mm_loadl_epi64(__m128i const* mem_addr)
который имеет глупый прототип (указатель на 16-байтовый тип__m128i
когда он действительно загружает только 8 байтов). Он доступен на всех 4 основных компиляторах x86 и должен быть безопасным. Обратите внимание, что__m128i*
просто передается этой непрозрачной внутренней части, а не разыменовывается.Более
_mm_loadu_si64 (void const* mem_addr)
также указан в списке, но в gcc этого нет.