Для одиночного скалярного произведения это просто вертикальное умножение и горизонтальная сумма (см. Самый быстрый способ выполнить горизонтальную векторную сумму с плавающей запятой на x86). hadd
стоит 2 перетасовки + 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 перетасовка на Intel, и больший выигрыш у AMD Jaguar / Bulldozer-family / Ryzen, потому что он сразу сужается до 128b вместо того, чтобы делать куча всего 256б. AMD разделяет операторы 256b на две операции по 128b.
Может быть полезно использовать hadd
в таких случаях, как параллельное выполнение 2 или 4 точечных произведений, когда вы используете его с двумя разными входными векторами. Норберта dot
из двух пар векторов выглядит оптимальным, если вы хотите получить сжатые результаты. Я не вижу способа добиться большего успеха даже с AVX2 vpermpd
в случайном порядке.
Конечно, если вам действительно нужен один больший dot
(из 8 или более double
s), используйте вертикальный add
(с несколькими аккумуляторами, чтобы скрыть vaddps
задержку) и в конце выполните горизонтальное суммирование. Вы также можете использовать fma
если доступно.
haddpd
внутренне перемешивает xy
и zw
вместе двумя разными способами и передает это в вертикальный addpd
, и это то, что мы в любом случае будем делать вручную. Если бы мы сохранили xy
и zw
отдельно, нам потребовалось бы 2 перемешивания + 2 добавления для каждого, чтобы получить точечный продукт (в отдельных регистрах). Таким образом, перемешивая их вместе с hadd
в качестве первого шага, мы экономим на общем количестве перемешиваний, только на добавлении и общем количестве мопов.
/* Norbert's 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
стоит в 2 раза больше, чем 128b hadd
, может иметь смысл сузить каждый продукт 256b до 128b отдельно, а затем объединить с хаддом 128b.
На самом деле, согласно таблицам Агнера Фога, haddpd xmm,xmm
составляет 4 мопса на Ryzen. (А версия 256b ymm - 8 мопс). Так что на самом деле лучше использовать 2x vshufpd
+ vaddpd
вручную на Ryzen, если эти данные верны. Возможно, это не так: его данные для Piledriver имеют 3 uop haddpd xmm,xmm
, и это только 4 uop с операндом памяти. Для меня не имеет смысла, что они не могли реализовать hadd
как только 3 (или 6 для ymm) моп.
Для выполнения 4 dot
с результатами, упакованными в один __m256d
, задается точная проблема, я думаю, что ответ @hirschhornsalz выглядит очень хорошо для процессоров Intel. Тщательно не изучал, но сочетать попарно с hadd
- это хорошо. vperm2f128
эффективен для Intel (но довольно плох для AMD: 8 мопов на Ryzen с пропускной способностью 1 на 3 с).
person
Peter Cordes
schedule
22.11.2017