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

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

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


person thnghh    schedule 07.02.2020    source источник
comment
@IwillnotexistIdonotexist: никогда не нужно VPHADDD для эффективных горизонтальных сумм. См. Самый быстрый способ выполнить горизонтальную векторную сумму с плавающей запятой на x86 (мой ответ также включает некоторые целочисленные версии). Вы просто хотите последовательно сужать, пока не дойдете до 1 элемента, с извлечением верхней полосы и затем перетасовкой в ​​пределах 128-битного вектора. Все ваши перемешивания могут принимать непосредственные управляющие операнды, а не векторы для _2_. например _3_, _4_ и _5_.   -  person Iwillnotexist Idonotexist    schedule 07.02.2020
comment
Вопрос @PeterCordes OP настолько обширен, что я не знаю, на что мы настраиваемся. Что оптимально / лучше? Я подчеркнул, что VPHADDD неизбежно генерирует ошибки, которые вы бы в любом случае видели с решением типа vpadd / vpermd, но, по крайней мере, это одна инструкция (размер кода). Если целью является пропускная способность, то, возможно, пакетирование 8 из этих заданий, транспонирование 8x8 в 24 инструкции перестановки и использование 7 вертикальных добавлений могут работать лучше всего (помогает, если ваш ЦП имеет два модуля перемешивания, тогда вы можете рассчитывать завершить пакет из 8 примерно за 12 + 4 = 16cc = амортизированные 2cc, иначе это 24 + 4 = 28cc = амортизированные 3.5cc, все еще неплохо).   -  person Peter Cordes    schedule 07.02.2020
comment
@IwillnotexistIdonotexist: все процессоры, поддерживающие AVX512 и VPHADDD, реализуют его как 2 перетасовки + 1 вертикальное добавление. (AMD до Zen2 декодировала его неэффективно для случая ymm, так как всего 8 мопов, а не 6. Или 4 мупа для версии xmm.) Так что да, транспонирование и хадд - это вариант использования для _2_. Вы не упомянули, что он декодирует больше перетасовок, чем необходимо для hsum одного вектора, и использование его таким образом является распространенной ошибкой оптимизации. (Или компромисс между размером кода и скоростью). Чрезмерное использование _3_ - распространенная ошибка.   -  person Iwillnotexist Idonotexist    schedule 07.02.2020
comment
Спасибо за очень подробный ответ. Доброго дня, Питер! Я сделал такую ​​же версию, как и ваша, но без зауженной части. Согласно вашему ответу, я должен его улучшить. Небольшой уточняющий вопрос: как можно сравнить время выполнения vphadd и vphadd? На веб-сайте Finder не указывается задержка для большинства функций AVX512.   -  person Peter Cordes    schedule 07.02.2020


Ответы (1)


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

Без AVX512 см. hsum_8x32(__m256i) ниже для AVX2 без reduce_add вспомогательной функции Intel. reduce_add в любом случае не обязательно оптимально компилировать с AVX512.


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

GCC 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

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

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


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

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


Эта общая стратегия подходит для всех типов элементов: float, double и любого целого числа.

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

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

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

всего упс: уменьшить: 4. Моя: 3

Особые случаи:

  • 8-битное целое число: начните с vpsadbw, более эффективно и избегайте переполнения, но затем продолжайте, как для 64-битных целых чисел.

  • 16-битное целое число: начните с расширения до 32 с помощью pmaddwd (_mm256_madd_epi16 с set1_epi16 (1)): SIMD: накопить Смежные пары - меньше мопов, даже если вас не волнует преимущество предотвращения переполнения, за исключением AMD до Zen2, где 256-битные инструкции стоят не менее 2 мопов. Но затем вы продолжаете как для 32-битного целого числа.

32-битное целое число может быть выполнено вручную, как это, с функцией SSE2, вызываемой функцией AVX2 после уменьшения до __m128i, в свою очередь вызываемой функцией AVX512 после уменьшения до __m256i. На практике вызовы, конечно же, будут встроены.

#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
}

// only needs AVX2
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 if AVX512 is enabled :/
    return hsum_epi32_avx(sum128);
}

// AVX512
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);
}

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

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

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

PS: анализ производительности _mm512_reduce_add_epi32 GCC по сравнению с clang (что эквивалентно моей версии) с использованием данных из https://uops.info/ и / или таблицы инструкций Agner Fog:

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

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

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

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

  • порты: уменьшить: 2p0, p5 (часть vpextrd), p0156 (скаляр add)
  • порты: мой: p5, p015 (vpadd на SKX), p0 (vmod)
  • перемешать 1 цикл - ›SIMD добавить 1 цикл -› vmovd 2 цикла

Общая стратегия для всех SSE / AVX / AVX512

  • vpextrd 3 цикла (параллельно с 2 циклами vmovd) - ›добавить 1 цикл.
  • Это похоже на микрооптимизацию. Насколько лучше вы рассчитываете сделать? Вы оптимизируете размер кода, количество инструкций, пропускную способность, задержку? Вы можете посмотреть инструкцию AVX2 _mm512_reduce_add_epu8 (горизонтальное сложение двойных слов из 256-битных векторов), но вы не можете обмануть свой выход из перестановки и добавить uops, до которого эта инструкция расширяется.
person Peter Cordes    schedule 07.02.2020
comment
@thnghh: Тайминги в руководстве Intel по внутренним функциям обычно правильные, но оно даже не дает подсчетов uop. (Руководство Intel Intrinsics - Задержка и пропускная способность). Это не более чем приблизительный ориентир. Обновил свой ответ со ссылками на uops.info (очень подробный и не должен содержать опечаток, потому что он сгенерирован компьютером) и Результаты экспериментов Агнера Фога (простой поиск, случайные опечатки и даже неточности, такие как пропущенные особые случаи). - person thnghh; 12.02.2020
comment
@ Питер Кордес: Респект! Есть ли хороший ответ на тот же вопрос для чисел с плавающей запятой одинарной и двойной точности? - person Peter Cordes; 12.02.2020
comment
@egyik: да, уже ссылка на этот ответ: Самый быстрый способ выполнить горизонтальную векторную сумму SSE (или другое сокращение). Как я сказал в этом ответе, просто используйте эквивалентные _1_ встроенные функции вместо _2_, чтобы уменьшить их до вектора _3_ или _4_. - person egyik; 25.03.2020
comment
Компилирует на Godbolt к этим инструкциям с GCC9.2 _32_ - person Peter Cordes; 26.03.2020