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

Я оптимизирую алгоритм размытия по Гауссу на изображении и хочу заменить использование буфера с плавающей запятой [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])];

person pseudomarvin    schedule 15.12.2015    source источник
comment
Аналогичный вопрос к stackoverflow.com/questions/32284106/. Этот более широкий и спрашивает о конкретной задаче обработки, но мой ответ там касается распаковки в float и обратно (для SSE, а не AVX). Предложение 16-битной фиксированной точки здесь также актуально, если вы можете использовать его вместо этого.   -  person Ivan Aksamentov - Drop    schedule 15.12.2015
comment
Я пробовал это с AVX2 на основе комментария, на который вы ссылались в VS2015, с _1_ Он не компилировался. Глядя на страницу встроенных функций Intel, в инструкции конкретно требуется __m128i. Есть ли причина полагать, что такой слепок, который я пробовал, подойдет? Я заметил, что VPMOVZXBD действительно принимает операнд памяти, поэтому странно, что инстринсик этого не делает.   -  person Peter Cordes    schedule 15.12.2015


Ответы (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 (_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
comment
@pseudomarvin: обратите внимание на оператор разыменования __m256i m = _mm256_cvtepu8_epi32 ((__m128i)(new_image + x)); перед приведением. Вам этого не хватает, поэтому вы передаете указатель на _2_, а не на _3_. Кроме того, я бы рекомендовал использовать _4_, чтобы снизить вероятность сбоя кода при компиляции с _5_. Если вам нужен код, который по-прежнему не будет выходить за пределы массива, даже с _6_, вы должны использовать _7_, но, как и в моем отчете об ошибке gcc, загрузка не будет складываться в операнд памяти для _8_. Вы правы, что это странный и плохой дизайн для встроенных функций. - person pseudomarvin; 16.12.2015
comment
При включенном USE_MOVQ _ 38_ (v5.3.0) испускает. (То же самое делает MSVC) - person Peter Cordes; 16.12.2015