Подтвердить что ты не робот

Intel AVX: 256-битная версия точечного продукта для переменных с плавающей запятой с двойной точностью

Расширенные векторные расширения Intel (AVX) не предлагают продукт dot в 256-битной версии (регистр YMM) для переменных с плавающей запятой с двойной точностью. "Почему?" вопрос был очень кратко рассмотрен на другом форуме (здесь) и на Qaru (here). Но вопрос, с которым я столкнулся, заключается в том, как эффективно заменить эту отсутствующую инструкцию другими инструкциями AVX?

Точечный продукт в 256-битной версии существует для переменных с плавающей запятой с одной точностью (ссылка здесь):

 __m256 _mm256_dp_ps(__m256 m1, __m256 m2, const int mask);

Идея состоит в том, чтобы найти эффективный эквивалент этой отсутствующей инструкции:

 __m256d _mm256_dp_pd(__m256d m1, __m256d m2, const int mask);

Чтобы быть более конкретным, код, который я хотел бы преобразовать из __m128 (четыре поплавка) в __m256d (4 удваивается), использует следующие инструкции:

   __m128 val0 = ...; // Four float values
   __m128 val1 = ...; //
   __m128 val2 = ...; //
   __m128 val3 = ...; //
   __m128 val4 = ...; //

   __m128 res = _mm_or_ps( _mm_dp_ps(val1,  val0,   0xF1),
                _mm_or_ps( _mm_dp_ps(val2,  val0,   0xF2),
                _mm_or_ps( _mm_dp_ps(val3,  val0,   0xF4),
                           _mm_dp_ps(val4,  val0,   0xF8) )));

Результатом этого кода является вектор _m128 четырех поплавков, содержащий результаты точечных произведений между val1 и val0, val2 и val0, val3 и val0, val4 и val0.

Может быть, это может дать подсказки для предложений?

4b9b3361

Ответ 1

Я бы использовал двойное умножение 4 *, а затем hadd (к сожалению, добавляет только 2 * 2 поплавка в верхней и нижней половине), извлеките верхнюю половину (тасование должно работать одинаково, возможно, быстрее) и добавить это в нижнюю половину.

Результат - в 64-битном разряде dotproduct.

__m256d xy = _mm256_mul_pd( x, y );
__m256d temp = _mm256_hadd_pd( xy, xy );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );

Edit:
После идеи Норберта П. я расширил эту версию, чтобы сделать 4 точечных продукта за один раз.

__m256d xy0 = _mm256_mul_pd( x[0], y[0] );
__m256d xy1 = _mm256_mul_pd( x[1], y[1] );
__m256d xy2 = _mm256_mul_pd( x[2], y[2] );
__m256d xy3 = _mm256_mul_pd( x[3], y[3] );

// low to high: xy00+xy01 xy10+xy11 xy02+xy03 xy12+xy13
__m256d temp01 = _mm256_hadd_pd( xy0, xy1 );   

// low to high: xy20+xy21 xy30+xy31 xy22+xy23 xy32+xy33
__m256d temp23 = _mm256_hadd_pd( xy2, xy3 );

// low to high: xy02+xy03 xy12+xy13 xy20+xy21 xy30+xy31
__m256d swapped = _mm256_permute2f128_pd( temp01, temp23, 0x21 );

// low to high: xy00+xy01 xy10+xy11 xy22+xy23 xy32+xy33
__m256d blended = _mm256_blend_pd(temp01, temp23, 0b1100);

__m256d dotproduct = _mm256_add_pd( swapped, blended );

Ответ 2

Я бы продолжил drhirsch answer для одновременного выполнения двух точечных продуктов, сохраняя некоторую работу:

__m256d xy = _mm256_mul_pd( x, y );
__m256d zw = _mm256_mul_pd( z, w );
__m256d temp = _mm256_hadd_pd( xy, zw );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );

Тогда dot(x,y) находится в низком двойном значении, а dot(z,w) находится в высоком двойном значении dotproduct.

Ответ 3

Для одного точечного произведения это просто вертикальная многократная и горизонтальная сумма (см. Самый быстрый способ сделать горизонтальную векторную сумму float на x86). hadd стоит 2 перетасовки + a add. Он почти всегда не оптимален для пропускной способности при использовании с обоими входами = одним и тем же вектором.

// both elements = dot(x,y)
__m128d dot1(__m256d x, __m256d y) {
    __m256d xy = _mm256_mul_pd(x, y);

    __m128d xylow  = _mm256_castps256_pd128(xy);   // (__m128d)cast isn't portable
    __m128d xyhigh = _mm256_extractf128_pd(xy, 1);
    __m128d sum1 =   _mm_add_pd(xylow, xyhigh);

    __m128d swapped = _mm_shuffle_pd(sum1, sum1, 0b01);   // or unpackhi
    __m128d dotproduct = _mm_add_pd(sum1, swapped);
    return dotproduct;
}

Если вам нужен только один точечный продукт, это лучше, чем @hirschhornsalz однопользовательский ответ на 1 shuffle uop на Intel и большую победу над AMD Jaguar/Bulldozer-family/Ryzen, потому что он сужается до 128b сразу же из всего, что связано с 256b. AMD расщепляет 256b операционных систем на два 128b uops.


Можно использовать hadd в случаях, например, при выполнении двух или четырех точечных продуктов параллельно, где вы используете его с двумя разными входными векторами. Норберт dot двух пар векторов выглядит оптимальным, если вы хотите, чтобы результаты были упакованы. Я не вижу никакого способа сделать лучше даже с AVX2 vpermpd в качестве перетаскивания переходов.

Конечно, если вы действительно хотите один более крупный dot (из 8 или более double s), используйте вертикальный add (с несколькими аккумуляторами, чтобы скрыть задержку vaddps), и выполните горизонтальное суммирование в конец. Вы также можете использовать fma, если он доступен.


haddpd внутренне перемещает xy и zw вместе два разных способа и каналы, которые соответствуют вертикальному addpd, и что мы будем делать вручную. Если бы мы сохраняли xy и zw раздельно, нам понадобилось бы 2 перетасовки + 2 добавления для каждого, чтобы получить точечный продукт (в отдельных регистрах). Поэтому, перетасовывая их вместе с hadd в качестве первого шага, мы сохраняем общее количество перетасовки, только при добавлении и суммарном счету uop.

/*  Norbert version, for an Intel CPU:
    __m256d temp = _mm256_hadd_pd( xy, zw );   // 2 shuffle + 1 add
    __m128d hi128 = _mm256_extractf128_pd( temp, 1 ); // 1 shuffle (lane crossing, higher latency)
    __m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 ); // 1 add
     // 3 shuffle + 2 add
*/

Но для AMD, где vextractf128 очень дешево, а 256b hadd стоит 2x столько же, сколько 128b hadd, имеет смысл сузить каждый продукт 256b до 128b отдельно, а затем объединить с 128b hadd.

На самом деле, согласно таблицы Agner Fog, haddpd xmm,xmm - 4 раза в Ryzen. (И версия 256b ymm - 8 часов). Так что на самом деле лучше использовать 2x vshufpd + vaddpd вручную на Ryzen, если эти данные верны. Это может быть не так: его данные для Piledriver имеют 3 uop haddpd xmm,xmm, и это всего 4 раза с операндом памяти. Для меня не имеет смысла, что они не могут реализовать hadd как только 3 (или 6 для ymm) uops.


Для выполнения 4 dot с результатами, упакованными в один __m256d, точная проблема была задана, я думаю, что ответ @hirschhornsalz выглядит очень хорошо для процессоров Intel. Я не изучал его очень тщательно, но сочетание в парах с hadd является хорошим. vperm2f128 эффективен для Intel (но довольно неплохо на AMD: 8 часов на Ryzen с пропускной способностью 1 к 3c).