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

Intel Advanced Vector Extensions (AVX) не предлагает точечного произведения в 256-битной версии (регистр YMM) для переменных с плавающей запятой двойной точности. "Почему?" вопрос был очень кратко рассмотрен на другом форуме (здесь) и при переполнении стека (здесь). Но вопрос, с которым я столкнулся, заключается в том, как эффективно заменить эту отсутствующую инструкцию другими инструкциями 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.

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


person gleeen.gould    schedule 04.05.2012    source источник
comment
Спасибо за идею, но я должен сохранить двойную точность в моем приложении.   -  person gleeen.gould    schedule 04.05.2012
comment
Кроме того, преобразование + скалярное произведение с плавающей точкой займет больше времени, чем произведение с двойной точкой.   -  person Gunther Piez    schedule 04.05.2012


Ответы (3)


Я бы использовал 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 );

Изменить:
После идеи Норберта П. я расширил эту версию, чтобы одновременно делать 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 );
person Gunther Piez    schedule 04.05.2012
comment
Спасибо за предложение, это хорошо работает. Я отредактировал свой вопрос, чтобы быть более конкретным. - person gleeen.gould; 04.05.2012
comment
Спасибо! Не могли бы вы объяснить последнюю строчку? Я не уверен, что понимаю это хорошо. Разве это не _mm256_add_pd? - person gleeen.gould; 05.05.2012
comment
@ gleeen.gould: Да, ты прав. Лучше посмотрите дважды, этот код не протестирован :-) Если вы используете gcc, вы можете использовать __v4df вместо __m256d и * или + вместо встроенных функций mul / add. Это работает хорошо, выбирает правильные векторные операции и позволяет избежать хлопот. - person Gunther Piez; 05.05.2012
comment
Мне все еще трудно это понять. Вместо двух последних строк я бы использовал следующее: __m256d swapped1 = _mm256_permute2f128_pd( temp01, temp23, 0x30 ); __m256d swapped2 = _mm256_permute2f128_pd( temp01, temp23, 0x21 ); __m256d dotproduct = _mm256_add_pd( swapped1, swapped2 ); Код непроверенный (у меня нет AVX на моем компьютере :-)) - person gleeen.gould; 05.05.2012
comment
@ gleeen.gould Это тоже должно сработать. В моем решении используется тот факт, что сложение коммутативно. Я добавлю еще несколько комментариев позже - person Gunther Piez; 05.05.2012
comment
@drhirsch: отличная идея. Но gleeen.gould прав, вам нужно дополнительное перемешивание. Рекомендую: __m256d swapped = _mm256_permute2f128_pd( temp01, temp23, 0x21 ); __m256d mixed = _mm256_blend_pd(temp01, temp23, 12); __m256d dotproduct = _mm256_add_pd( swapped, mixed );. Единственная причина в том, что VPERM2F128 занимает 2 цикла против 1 цикла VBLENDPD. (Надеюсь, я правильно понял константы) - person Norbert P.; 05.05.2012
comment
@NorbertP. Спасибо, я не знал VBLENDPD. - person gleeen.gould; 05.05.2012
comment
Спасибо за вклад. Я обновил его до последней версии. BLENDPD - хорошая идея - person Gunther Piez; 05.05.2012
comment
@drhirsch: проверьте имена переменных в последних двух строках, и тип dotproduct должен быть __m256d. ;) - person Norbert P.; 06.05.2012
comment
@NorbertP. : только что проверил константы для VPERM2F128 и VBLENDPD (используя внутреннее руководство для Intel AVX2 v2.6.2, здесь). Задержки равны 1 для них обоих, пропускная способность - 1 для VPERM2F128 и 0,5 для VBLENDPD. - person gleeen.gould; 06.05.2012
comment
@ gleeen.gould: AVX2 уже вышел? Я думал, что появится в Haswell в 2013 году. Я говорил о текущем поколении AVX на Sandy Bridge: см. таблицы инструкций Агнера Фога , п. 129. - person Norbert P.; 07.05.2012
comment
Примечание. Даже с AVX2 и vpermpd решение, использующее vextractf128 и addpd, имеет меньшую суммарную задержку, чем последующее применение vpermpd и vhaddpd для генерации горизонтальной суммы. - person Pixelchemist; 13.03.2015
comment
Ваше одно-векторное точечное произведение неэффективно (не используйте hadd с одним и тем же входом дважды.) Я думаю, что ваше решение с четырехточечным произведением выглядит великолепно. - person Peter Cordes; 23.11.2017

Я бы расширил ответ дрхирша для одновременного выполнения двух точечных произведений, сэкономив при этом некоторую работу:

__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.

person Norbert P.    schedule 05.05.2012

Для одиночного скалярного произведения это просто вертикальное умножение и горизонтальная сумма (см. Самый быстрый способ выполнить горизонтальную векторную сумму с плавающей запятой на 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 или более doubles), используйте вертикальный 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