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
.
Может быть, это может дать подсказки для предложений?
Ответы
Ответ 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).