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

Posted

技术标签:

【中文标题】使用 AVX512 或 AVX2 计算所有压缩 32 位整数之和的最快方法【英文标题】:Fastest method to calculate sum of all packed 32-bit integers using AVX512 or AVX2 【发布时间】:2020-02-07 07:08:13 【问题描述】:

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

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

【问题讨论】:

这似乎是一个微优化。你期望做得好多少?您是否正在针对代码大小、指令数量、吞吐量、延迟进行优化?您可以查看的指令是 AVX2 VPHADDD(从 256 位向量水平添加双字),但您不能欺骗自己的出路并添加 uops 该指令扩展为。 @IwillnotexistIdonotexist:你永远不需要VPHADDD 来获得有效的水平总和。请参阅Fastest way to do horizontal float vector sum on x86(我的回答还包括一些整数版本)。您只想连续缩小直到减少到 1 个元素,然后提取高通道,然后在 128 位向量中随机播放。您所有的洗牌都可以立即控制操作数,而不是vpermd 的向量。例如VEXTRACTI32x8vextracti128vpshufd @PeterCordes OP 的问题太广泛了,我不知道我们在调整什么。什么是“最佳”/“最佳”?我确实强调了VPHADDD 不可避免地会生成您在使用 vpadd/vpermd 类型的解决方案时已经看到的微指令,但至少它是一条指令(代码大小)。如果吞吐量是目标,那么可能批处理 8 个这些作业,在 24 个置换指令中转置 8x8 并使用 7 个垂直添加可能效果最好(如果您的 CPU 有两个 shuffle 单元,那么您可以期望在大约12+4=16cc=摊销2cc,不然就是24+4=28cc=摊销3.5cc,还是挺不错的)。 @IwillnotexistIdonotexist:所有支持 AVX512 和 vphadd 的 CPU 都将其实现为 2 个 shuffle uop + 1 个垂直添加 uop。 (Zen2 之前的 AMD 对 ymm 情况的解码效率特别低,因为总共 8 个 uops 而不是 6 个。或者 xmm 版本的 4 个 uops。)所以是的,转置和 hadd 是vphadd 的一个用例。您没有提到它解码为比一个向量的 hsum 所需的更多的随机播放,并且以这种方式使用它是一种常见的错过优化。 (或代码大小与速度的权衡)。过度使用phadd 是一个常见的错误。 【参考方案1】:

相关:如果您正在寻找不存在的 _mm512_reduce_add_epu8,请参阅 Summing 8-bit integers in __m512i with AVX intrinsics vpsadbw,因为 qwords 中的 hsum 比改组更有效。

如果没有 AVX512,请参阅下面的 hsum_8x32(__m256i) 了解没有 Intel 的 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 + a movd

Clang 不会那样做;它又做了一个 shuffle / SIMD add 步骤,以减少 vmovd 的单个标量。请参阅下面对两者的性能分析。


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

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


所有 SSE / AVX / AVX512 的一般策略

您希望从 512 -> 256、256 -> 128 依次缩小,然后在 __m128i 内随机播放,直到缩小到一个标量元素。据推测,未来的某些 AMD CPU 会将 512 位指令解码为两个 256 位 uop,因此减少宽度是一个巨大的胜利。更窄的指令大概会消耗更少的能量。

您的 shuffle 可以立即控制操作数,而不是 vpermd 的向量。 例如VEXTRACTI32x8vextracti128vpshufd。 (或vpunpckhqdq 以节省直接常量的代码大小。)

见Fastest way to do horizontal SSE vector sum (or other reduction)(我的回答还包括一些整数版本)。

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

特殊情况:

8 位整数:以vpsadbw 开头,效率更高,避免溢出,然后继续处理 64 位整数。

16 位整数:首先使用 pmaddwd (_mm256_madd_epi16 with set1_epi16(1)) 扩大到 32:SIMD: Accumulate Adjacent Pairs - 即使您不关心避免溢出的好处,也可以减少微指令,除非Zen2 之前的 AMD,其中 256 位指令至少需要 2 微秒。但是你继续 32 位整数。

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

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

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

但是越早减小到更窄的向量宽度通常是好的,也许越早从系统中取出 512 位微指令,这样 CPU 就可以重新激活端口 1 上的 SIMD 执行单元,如果你不做更多 512-马上开始工作。

将 on 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

P.S.:使用来自 https://uops.info/ 和/或 Agner Fog's instruction tables 的数据对 GCC 的 _mm512_reduce_add_epi32 与 clang 的(相当于我的版本)进行性能分析:

内联到对结果执行某些操作的调用者后,它可以允许优化,例如添加常量以及使用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 个周期时相等:

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

【讨论】:

感谢您非常详细的回答。祝彼得有美好的一天!我做了一个和你一样的版本,但是没有缩小范围。根据你的回答,我应该改进它。一个小的后续问题:如何比较vextracti64x2vextracti128 的运行时间? finder 网站未指定大多数 AVX512 功能的延迟。 @thnghh:英特尔内在函数指南中的时序通常是正确的,但它甚至没有给出 uop 计数。 (Intel Intrinsics guide - Latency and Throughput)。除了粗略的指导方针之外,它没有用处。用uops.info 的链接更新了我的答案(非常详细,并且应该没有拼写错误,因为它是机器生成的)和 Agner Fog 的实验结果(易于搜索,偶尔的拼写错误,甚至像错过的特殊情况这样的不准确)。 @Peter Cordes:尊重!对单精度和双精度浮点数的同一个问题有好的答案吗? @egyik:是的,已经从这个答案链接:Fastest way to do horizontal SSE vector sum (or other reduction)。就像我在这个答案中所说的那样,只需使用等效的 _ps 内在函数而不是 _epi32 来减少到 __m128__m128d 向量。

以上是关于使用 AVX512 或 AVX2 计算所有压缩 32 位整数之和的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

AVX2 和 AVX-512 有啥区别?

如何编译 TensorFlow 二进制文件以使用 AVX2、AVX512F、FMA?

AVX2 上的 256 位 CRC 计算

使用 AVX512 或 SVML 内在函数将压缩的 16 位整数与掩码相除

如何计算avx和avx2指令集的数量

使用 AVX2 计算 8 个长整数的最小值