使用 AVX512 或 AVX2 计算所有打包 32 位整数总和的最快方法

thn*_*ghh 5 c intrinsics avx avx2 avx512

我正在寻找一种最佳方法来计算 a __m256ior中所有打包的 32 位整数的总和__m512i。为了计算n 个元素的总和,我经常使用log2(n) vpadddvpermd函数,然后提取最终结果。但是,这不是我认为的最佳选择。

编辑:在速度/周期减少方面最佳/最佳。

Pet*_*des 12

相关:如果您正在寻找不存在的_mm512_reduce_add_epu8,请参阅Summing 8-bit integers in __m512i with AVX内在函数 vpsadbw作为 qwords 中的 hsum 比洗牌更有效。

如果没有 AVX512,请参阅hsum_8x32(__m256i)下面的没有 Intelreduce_add辅助功能的 AVX2。 reduce_add无论如何,不​​一定能用 AVX512 进行最佳编译。


中有一个int _mm512_reduce_add_epi32(__m512i)内联函数immintrin.h。你不妨使用它。(它编译为随机播放和添加指令,但比 更有效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
Run Code Online (Sandbox Code Playgroud)

提取两次来提供标量加法是有问题的;p0 和 p5 需要 uop,因此它相当于常规的 shuffle + a movd

Clang 不这样做;它又进行了一步洗牌/SIMD 添加,以减少到单个标量vmovd。两者的性能分析见下文。


有一个VPHADDD,但你不应该在两个输入相同的情况下使用它。(除非您针对代码大小而不是速度进行优化)。对多个向量进行转置和求和可能很有用,从而产生一些结果向量。您可以通过使用phadd2 个不同的输入来实现这一点。(除了 256 和 512 位会变得混乱,因为vphadd仍然只是在通道内。)

是的,你需要log2(vector_width)洗牌和vpaddd指导。 (所以这不是很有效;避免内部循环内的水平求和。例如,垂直累加直到循环结束)。


所有 SSE / AVX / AVX512 的通用策略

您想要从 512 -> 256 连续缩小,然后是 256 -> 128,然后在其中进行洗牌,__m128i直到缩小到一个标量元素。据推测,未来的一些 AMD CPU 会将 512 位指令解码为两个 256 位微指令,因此减少宽度是一个巨大的胜利。而且更窄的指令可能会消耗更少的电量。

您的洗牌可以立即控制操作数,而不是 的向量vpermd 例如VEXTRACTI32x8vextracti128、 和vpshufd。(或者vpunpckhqdq节省立即常量的代码大小。)

请参阅执行水平 SSE 向量和(或其他缩减)的最快方法 (我的答案还包括一些整数版本)。

此通用策略适用于所有元素类型:浮点型、双精度型和任意大小的整数

特别案例:

  • 8 位整数:以 开头vpsadbw,更高效并避免溢出,但随后继续为 64 位整数。

  • 16 位整数:首先使用pmaddwd( _mm256_madd_epi16with set1_epi16(1)) 扩大到 32:SIMD:累积相邻对- 即使您不关心避免溢出的好处,也会减少微指令,除了在 Zen2 之前的 AMD 上,其中 256 位指令至少花费 2 uop。但随后您继续处理 32 位整数。

32位整数可以像这样手动完成,在减少到 后由 AVX2 函数调用 SSE2 函数__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);
}
Run Code Online (Sandbox Code Playgroud)

请注意,这使用__m256ihsum 作为__m512i;的构建块。首先进行车道内操作没有任何好处。

这可能是一个非常微小的优势:车道内洗牌比车道交叉具有更低的延迟,因此它们可以提前执行 2 个周期并提前离开 RS,同样也可以稍早从 ROB 退出。但即使您这样做了,更高延迟的洗牌也会在几个指令后出现。因此,如果此 hsum 位于关键路径上(阻止退出),您可能会提前 2 个周期将一些独立指令放入后端。

但越早减少到更窄的向量宽度通常是好的,如果您没有正确执行更多 512 位工作,也许可以更快地从系统中获取 512 位 uops,以便 CPU 可以重新激活端口 1 上的 SIMD 执行单元离开。

使用 GCC9.2在 Godbolt 上编译这些指令-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
Run Code Online (Sandbox Code Playgroud)

_mm512_reduce_add_epi32PS:使用https://uops.info/和/或Agner Fog 指令表中的数据对 GCC与 clang(相当于我的版本)进行性能分析:

内联到对结果执行某些操作的调用者之后,它可以允许进行优化,例如添加常量以及使用lea eax, [rax + rdx + 123]等。

但除此之外,它似乎总是比我在 Skylake-X 上实现结束时的 shuffle / vpadd / vmovd 更糟糕:

  • 总 uops:减少:4。我的:3
  • 端口:减少:2p0、p5(vpextrd 的一部分)、p0156(标量add
  • 端口:我的:p5、p015(vpadd在 SKX 上)、p0 ( vmod)

假设没有资源冲突,4 个周期的延迟相等:

  • shuffle 1 个周期 -> SIMD 添加 1 个周期 -> vmovd 2 个周期
  • vpextrd 3 个周期(与 2 个周期 vmovd 并行)-> 添加 1 个周期。