Загрузка 8 символов из памяти в переменную __m256 в виде упакованных одиночных прецизионных поплавков

Я оптимизирую алгоритм размытия Gaussian на изображении, и я хочу заменить использование поплавкового буфера [8] в коде ниже с внутренней переменной __m256. Какая серия инструкций лучше всего подходит для этой задачи?

// unsigned char *new_image is loaded with data
...
  float buffer[8];

  buffer[x ]      = new_image[x];       
  buffer[x + 1] = new_image[x + 1]; 
  buffer[x + 2] = new_image[x + 2]; 
  buffer[x + 3] = new_image[x + 3]; 
  buffer[x + 4] = new_image[x + 4]; 
  buffer[x + 5] = new_image[x + 5]; 
  buffer[x + 6] = new_image[x + 6]; 
  buffer[x + 7] = new_image[x + 7]; 
 // buffer is then used for further operations
...

//What I want instead in pseudocode:
 __m256 b = [float(new_image[x+7]), float(new_image[x+6]), ... , float(new_image[x])];

Ответы

Ответ 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:


Не существует встроенного использования 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);
}

При включенном USE_MOVQ выдается %23include+ //%23if+0 %23if+!!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%0A++#define USE_MOVQ #endif #define USE_MOVQ //+Comment this to let the evil #ifndef have an effect //#undef USE_MOVQ __m256+load_bytes_to_m256(uint8_t+*p) { #ifdef USE_MOVQ //+Compiles to an actual movq then movzx ymm,xmm with gcc8.3+-O3%0A++ __m128i small_load = _mm_loadl_epi64( (const __m128i*)p); %23else++//USE_LOADU//+Compiles to a 128b load with gcc -O0, potentially segfaulting%0A++ __m128i small_load = _mm_loadu_si128( (const __m128i*)p+); #endif%0A++%0A++ __m256i intvec = _mm256_cvtepu8_epi32( small_load );%0A++ //__m256i intvec = _mm256_cvtepu8_epi32( *(__m128i*)p+)%3B++//+Compiles to an aligned load with -O0%0A++ return _mm256_cvtepi32_ps(intvec); } '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:35.31960142338032,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:g530,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c++,libs:!(),options:'-Wall -O3 -march=haswell',source:1),l:'5',n:'0',o:'x86-64 gcc 5.3+(Editor #1,+Compiler+#1)+C++',t:'0')),k:31.34706524328636,l:'4',m:50,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:vcpp_v19_14_x64,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c++,libs:!(),options:'-O2',source:1),l:'5',n:'0',o:'x64 msvc v19.14+(Editor #1,+Compiler+#3)+C++',t:'0')),header:(),l:'4',m:50,n:'0',o:'',s:0,t:'0')),k:31.34706524328636,l:'3',n:'0',o:'',t:'0'),(g:!((g:!((h:compiler,i:(compiler:clang391,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c++,libs:!(),options:'-Wall -O3 -march=haswell',source:1),l:'5',n:'0',o:'x86-64+Clang 3.9.1+(Editor #1,+Compiler+#2)+C++',t:'0')),k:33.33333333333333,l:'4',m:62.5,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:icc1301,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c++,libs:!(),options:'-Wall -O3 -march=haswell',source:1),l:'5',n:'0',o:'x86-64 icc 13.0.1+(Editor #1,+Compiler+#4)+C++',t:'0')),header:(),l:'4',m:37.5,n:'0',o:'',s:0,t:'0')),k:33.33333333333333,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer"> gcc -O3 (v5.3.0). (Так же, как и MSVC)

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 этого нет.