Самый быстрый метод для вычисления суммы всех упакованных 32-битных целых чисел с использованием AVX512 или AVX2 - PullRequest
4 голосов
/ 07 февраля 2020

Я ищу оптимальный метод для вычисления суммы всех упакованных 32-битных целых чисел в __m256i или __m512i. Для вычисления суммы n элементов я обычно использую функции log2 (n) vpaddd и vpermd, а затем извлекаю окончательный результат. Однако, я думаю, это не лучший вариант.

Редактировать: лучший / оптимальный с точки зрения уменьшения скорости / цикла.

1 Ответ

5 голосов
/ 07 февраля 2020

(Связано: если вы ищете несуществующий _mm512_reduce_add_epu8, см. Суммирование 8-битных целых чисел в __m512i с внутренними AVX ; vpsadbw в качестве значения hsum в qwords гораздо эффективнее чем перетасовка.)


В immintrin.h есть встроенная функция int _mm512_reduce_add_epi32(__m512i). Вы могли бы также использовать это. (Он компилируется для перемешивания и добавления инструкций, но более эффективен, чем vpermd, как я опишу ниже.) AVX512 не представил никакой новой аппаратной поддержки для горизонтальных сумм, только этот новый помощник функция. Это все еще что-то, чтобы избежать когда бы ни было возможно.

G CC 9.2 -O3 -march=skylake-avx512 компилирует обертку, которая вызывает это следующим образом:

        vextracti64x4   ymm1, zmm0, 0x1
        vpaddd  ymm1, ymm1, ymm0
        vextracti64x2   xmm0, ymm1, 0x1   # silly compiler, vextracti128 would be shorter
        vpaddd  xmm1, xmm0, xmm1
        vpshufd xmm0, xmm1, 78
        vpaddd  xmm0, xmm0, xmm1

        vmovd   edx, xmm0
        vpextrd eax, xmm0, 1              # 2x xmm->integer to feed scalar add.
        add     eax, edx
        ret

Извлечение дважды для подачи скаляра добавить сомнительно; для p0 и p5 нужны мопы, поэтому это эквивалентно обычному перемешиванию + a movd.

Clang этого не делает; он делает еще один шаг добавления shuffle / SIMD, чтобы уменьшить до одного скаляра для vmovd. Ниже приведен анализ этих двух параметров.


Существует VPHADDD, но вы никогда не должны использовать его с обоими входами одинаково. (Если вы не оптимизируете размер кода по скорости). Может быть полезно транспонировать и суммировать несколько векторов, что приводит к некоторым векторам результатов. Вы делаете это путем подачи phadd 2 различных входов. (За исключением того, что он запутывается с 256 и 512 битами, потому что vphadd по-прежнему только на линии.)

Да, вам нужны log2(vector_width) шаффлы и vpaddd инструкции. ( Так что это не очень эффективно, избегайте горизонтальных сумм внутри внутренних циклов. Накапливайте по вертикали до конца al oop, например).

Вы хотите последовательно сузить от 512 -> 256, затем 256 -> 128, затем перетасуйте в пределах __m128i, пока не получите один скалярный элемент . Предположительно некоторые будущие процессоры AMD будут декодировать 512-битные инструкции в два 256-битных мопа, поэтому уменьшение ширины является большой победой. Более узкие инструкции, вероятно, стоят немного меньше энергии.

Ваши тасовки могут принимать непосредственные управляющие операнды, а не векторы для vpermd. , например, VEXTRACTI32x8, vextracti128 и vpshufd. (Или vpunpckhqdq, чтобы сохранить размер кода для непосредственной константы.)

См. Самый быстрый способ сделать горизонтальную векторную сумму с плавающей запятой на x86 (мой ответ также включает несколько целочисленных версий).

Эта общая стратегия подходит для всех типов элементов: с плавающей запятой, двойных чисел и целых чисел любого размера (кроме 8-битных, где вы используете vpsadbw)

#include <immintrin.h>
#include <stdint.h>

// from my earlier answer, with tuning for non-AVX CPUs removed
// static inline
uint32_t hsum_epi32_avx(__m128i x)
{
    __m128i hi64  = _mm_unpackhi_epi64(x, x);           // 3-operand non-destructive AVX lets us save a byte without needing a movdqa
    __m128i sum64 = _mm_add_epi32(hi64, x);
    __m128i hi32  = _mm_shuffle_epi32(sum64, _MM_SHUFFLE(2, 3, 0, 1));    // Swap the low two elements
    __m128i sum32 = _mm_add_epi32(sum64, hi32);
    return _mm_cvtsi128_si32(sum32);       // movd
}

uint32_t hsum_8x32(__m256i v)
{
    __m128i sum128 = _mm_add_epi32( 
                 _mm256_castsi256_si128(v),
                 _mm256_extracti128_si256(v, 1)); // silly GCC uses a longer AXV512VL instruction :/
    return hsum_epi32_avx(sum128);
}

uint32_t hsum_16x32(__m512i v)
{
    __m256i sum256 = _mm256_add_epi32( 
                 _mm512_castsi512_si256(v),  // low half
                 _mm512_extracti64x4_epi64(v, 1));  // high half.  AVX512F.  32x8 version is AVX512DQ
    return hsum_8x32(sum256);
}

Обратите внимание, что здесь используется __m256i хсум как строительный блок для __m512i; вначале ничего нельзя получить, выполняя операции в полосе.

Возможно, это очень маленькое преимущество: задержки в полосе имеют меньшую задержку, чем при пересечении полосы, поэтому они могут выполнить на 2 цикла раньше и покинуть RS раньше. и аналогичным образом удалиться из ROB немного раньше. Но тасовки с более высокой задержкой появятся через пару инструкций, даже если вы это сделали. Таким образом, вы могли бы получить несколько независимых инструкций для серверных 2-х циклов ранее, если этот hsum находился на критическом пути (блокирование выхода на пенсию).

Но сокращение до более узкой векторной ширины раньше, как правило, хорошо, возможно получение 512-битных мопов из системы быстрее, чтобы процессор мог повторно активировать исполнительные блоки SIMD на порту 1, если вы не выполняете больше 512-битной работы прямо сейчас.

Компилирует вкл Godbolt к этим инструкциям, с GCC9.2 -O3 -march=skylake-avx512

hsum_16x32(long long __vector(8)):
        vextracti64x4   ymm1, zmm0, 0x1
        vpaddd  ymm0, ymm1, ymm0
        vextracti64x2   xmm1, ymm0, 0x1   # silly compiler uses a longer EVEX instruction when its available (AVX512VL)
        vpaddd  xmm0, xmm0, xmm1
        vpunpckhqdq     xmm1, xmm0, xmm0
        vpaddd  xmm0, xmm0, xmm1
        vpshufd xmm1, xmm0, 177
        vpaddd  xmm0, xmm1, xmm0
        vmovd   eax, xmm0
        ret

PS: анализ перфомента G CC '_mm512_reduce_add_epi32 против Clang's (что эквивалентно моей версии), используя данные из https://uops.info/ и / или таблиц инструкций Agner Fog :

После встраивания в вызывающую программу, которая что-то делает с В результате, это может позволить оптимизацию, такую ​​как добавление константы, используя lea eax, [rax + rdx + 123] или что-то в этом роде.

Но в остальном он кажется почти всегда хуже, чем shuffle / vpadd / vmovd в конце моей реализации, на скайле ke-X:

  • всего моп: уменьшить: 4. Шахта: 3
  • порты: уменьшить: 2p0, p5 (часть vpextrd), p0156 (скаляр add)
  • порты: мой: p5, p015 (vpadd на SKX), p0 (vmod)

Задержка равна 4 циклам, при условии отсутствия конфликтов ресурсов:

  • перемешать 1 цикл -> SIMD добавить 1 цикл -> vmovd 2 цикла
  • vpextrd 3 цикла (параллельно с 2 циклами vmovd) -> добавить 1 цикл.
...