c-使用AVX512或AVX2计算所有压缩32位整数之和的最快方法



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

编辑:速度/循环减少方面的最佳/最佳。

相关:如果您正在寻找不存在的_mm512_reduce_add_epu8,请参阅将__m512i中的8位整数与AVX内部函数vpsadbw相加作为qword中的hsum比混洗更有效。

如果没有AVX512,请参阅下面的hsum_8x32(__m256i),了解没有英特尔reduce_add辅助函数的AVX2。无论如何,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

提取两次以馈送标量加法是有问题的;它需要p0和p5的uops,所以它相当于一个常规的shuffle+一个movd

Clang不这么做;它再进行一步混洗/SIMD相加以减少到CCD_ 15的单个标量。请参阅以下内容以了解两者的性能分析。


有一个VPHADDD,但您永远不应该在两个输入相同的情况下使用它。(除非您正在针对代码大小过快进行优化)。对多个向量进行转置和求和可能很有用,从而产生一些结果向量。您可以通过向phadd提供2个不同的输入来实现这一点。(除了由于vphadd仍然只在通道中,所以它在256和512位时变得混乱。)

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


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

您希望从512开始依次缩小范围->256,然后256->128,然后在__m128i中混洗,直到减少到一个标量元素。据推测,一些未来的AMD CPU将把512位指令解码为两个256位的uop,所以减少宽度是一个巨大的胜利。而较窄的指令可能会花费略低的功率。

您的shuffle可以获取立即控制操作数,而不是vpermd的矢量例如VEXTRACTI32x8vextracti128vpshufd。(或者vpunpckhqdq为立即常数保存代码大小。)

请参阅做水平SSE矢量和(或其他归约)的最快方法(我的答案还包括一些整数版本)。

这种通用策略适用于所有元素类型:float、double和任何大小的整数

特殊情况:

  • 8位整数:从vpsadbw开始,效率更高,避免溢出,但随后继续作为64位整数。

  • 16位整数:首先使用pmaddwd将其扩展到32(使用set1_epi16(1)的_mm256_madd_epi16):SIMD:累加相邻对-即使您不关心避免溢出的好处,也可以减少uops,Zen2之前的AMD除外,在Zen2之前,256位指令的成本至少为2 uops。但是,您继续使用32位整数。

32位整数可以像这样手动完成,其中SSE2函数在减少到__m128i后由AVX2函数调用,而在减少到__m256i后由AVX512函数调用。当然,这些调用将在实践中内联。

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

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

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

但是,如果不立即进行更多的512位工作,那么更快地减少到更窄的矢量宽度通常是好的,也许可以更快地从系统中取出512位uop,这样CPU就可以重新激活端口1上的SIMD执行单元。

使用GCC9.2-O3 -march=skylake-avx512在Godbolt上按照这些说明进行编译

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

p。S.:GCC的_mm512_reduce_add_epi32与clang的(相当于我的版本)的性能分析,使用https://uops.info/和/或Agner Fog的指令表:

在内联到对结果执行某些操作的调用程序之后,它可以允许优化,比如使用lea eax, [rax + rdx + 123]或其他方法添加常量。

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

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

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

  • 洗牌1个周期->SIMD加1个周期->vmovd 2个周期
  • vpextrd 3个循环(与2个循环vmovd并行)->添加1个循环

最新更新