Самый эффективный способ получить __m256 горизонтальных сумм из 8 исходных векторов __m256

Я знаю, как суммировать один __m256, чтобы получить одно суммарное значение. Однако у меня есть 8 векторов, таких как Ввод

1: a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7],
.....,
.....,
8: h[0], h[1], h[2], h[3], h[4], a[5], a[6], a[7]

Выход

a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7], 
 ...., 
h[0]+h[1]+h[2]+h[3]+h[4]+h[5]+h[6]+h[7]

Мой метод. Любопытно, есть ли лучший способ.

            __m256 sumab = _mm256_hadd_ps(accumulator1, accumulator2);
            __m256 sumcd = _mm256_hadd_ps(accumulator3, accumulator4);

            __m256 sumef = _mm256_hadd_ps(accumulator5, accumulator6);
            __m256 sumgh = _mm256_hadd_ps(accumulator7, accumulator8);

            __m256 sumabcd = _mm256_hadd_ps(sumab, sumcd);
            __m256 sumefgh = _mm256_hadd_ps(sumef, sumgh);

            __m128 sumabcd1 = _mm256_extractf128_ps(sumabcd, 0);
            __m128 sumabcd2 = _mm256_extractf128_ps(sumabcd, 1);
            __m128 sumefgh1 = _mm256_extractf128_ps(sumefgh, 0);
            __m128 sumefgh2 = _mm256_extractf128_ps(sumefgh, 1);

            sumabcd1 = _mm_add_ps(sumabcd1, sumabcd2);
            sumefgh1 = _mm_add_ps(sumefgh1, sumefgh2);

 __m256 result =_mm256_insertf128_ps(_mm256_castps128_ps256(sumabcd1), sumefgh1, 1)

Ответ 1

Вы можете использовать 2x _mm256_permute2f128_ps для выравнивания низких и высоких полос для вертикального vaddps. Это вместо 2x extractf128/insertf128. Это также превращает две команды 128b vaddps xmm в один 256b vaddps ymm.

vperm2f128 работает так же быстро, как один vextractf128 или vinsertf128 на процессорах Intel. Однако он замедляется на AMD (8 м-операционных систем с задержкой 4 с на семействе Bulldozer). Тем не менее, не так уж плохо, что вам нужно его избегать, даже если вы заботитесь о перфомансе на AMD. (И одна из перестановок на самом деле может быть vinsertf128).


__m256 hsum8(__m256 a, __m256 b, __m256 c, __m256 d,
             __m256 e, __m256 f, __m256 g, __m256 h)
{
    // a = [ A7 A6 A5 A4 | A3 A2 A1 A0 ]
    __m256 sumab = _mm256_hadd_ps(a, b);
    __m256 sumcd = _mm256_hadd_ps(c, d);

    __m256 sumef = _mm256_hadd_ps(e, f);
    __m256 sumgh = _mm256_hadd_ps(g, h);

    __m256 sumabcd = _mm256_hadd_ps(sumab, sumcd);  // [ D7:4 ... A7:4 | D3:0 ... A3:0 ]
    __m256 sumefgh = _mm256_hadd_ps(sumef, sumgh);  // [ H7:4 ... E7:4 | H3:0 ... E3:0 ]

    __m256 sum_hi = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x31);  // [ H7:4 ... E7:4 | D7:4 ... A7:4 ]
    __m256 sum_lo = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x20);  // [ H3:0 ... E3:0 | D3:0 ... A3:0 ]

    __m256 result = _mm256_add_ps(sum_hi, sum_lo);
    return result;
}

Этот компилируется, как вы ожидали. Второй permute2f128 фактически компилируется в vinsertf128, поскольку он использует только нижнюю полосу каждого входа таким же образом, что и vinsertf128. gcc 4.7 и позже делают эту оптимизацию, но только более поздние версии clang (v3.7). Если вы заботитесь о старом кланге, сделайте это на исходном уровне.

Экономия в исходных линиях больше, чем экономия в инструкциях, потому что _mm256_extractf128_ps(sumabcd, 0); скомпилирует нулевые инструкции: это просто бросок. Никакой компилятор не должен выделять vextractf128 с помощью imm8, кроме 1. (vmovdqa xmm/m128, xmm всегда лучше для получения низкой полосы). Хорошая работа Intel по расходованию байта инструкции на будущую проверку, которую вы не могли использовать, потому что простые префиксы VEX не имеют места для кодирования более длинных векторов.

Две команды vaddps xmm могут выполняться параллельно, поэтому использование одиночного vaddps ymm в основном - это просто пропускная способность (и размер кода), а не латентность.

Мы сбрасываем 3 цикла из полного исключения окончательного vinsertf128.


vhaddps - это 3 часа, 5 секунд ожидания и 1 на 2 c пропускную способность. (Латентность 6 с на Skylake). Два из этих трех пусков работают на порту тасования. Я предполагаю, что он в основном делает 2x shufps для генерации операндов для addps.

Если мы сможем эмулировать haddps (или, по крайней мере, получить горизонтальную операцию, которую мы можем использовать) с одним shufps/addps или чем-то, мы бы вышли вперед. К сожалению, я не понимаю, как это сделать. Единственный случайный выбор может дать только один результат с данными из двух векторов, но нам нужны оба входа в вертикальный addps, чтобы иметь данные от обоих векторов.

Я не думаю, что выполнение горизонтальной суммы выглядит по-другому перспективным. Как правило, hadd не является хорошим выбором, поскольку обычный случай использования в горизонтальной сумме касается только одного элемента его вывода. Это не так: каждый элемент каждого результата hadd фактически используется.