【问题标题】:Get sum of values stored in __m256d with SSE/AVX使用 SSE/AVX 获取存储在 __m256d 中的值的总和
【发布时间】:2018-10-01 04:06:37
【问题描述】:

有没有办法获取存储在 __m256d 变量中的值的总和?我有这个代码。

acc = _mm256_add_pd(acc, _mm256_mul_pd(row, vec));
//acc in this point contains {2.0, 8.0, 18.0, 32.0}
acc = _mm256_hadd_pd(acc, acc);
result[i] = ((double*)&acc)[0] + ((double*)&acc)[2];

此代码有效,但我想用 SSE/AVX 指令替换它。

【问题讨论】:

  • 我对链接副本的回答实际上没有 AVX _pd 版本,但它确实显示了如何提取 __m256 向量的高 128。它解释了横向缩小每一步缩小一半的总体思路。
  • 顺便说一句,如果方便,请考虑为result[i + 0..3] 并行生成 4 个结果,以避免需要每个元素的水平总和。
  • 已重新打开,因为我刚刚为双打准备了一个解决方案,我想发布作为答案。
  • 常用技术:编写标量代码,编译,读取生成的asm(本例中,gcc生成vhaddpd+vperm2f128+vaddpd)。
  • 在我的测试中,数据一开始就在内存中,而不是在寄存器中。调整 znver1 然后要么不矢量化,要么矢量化 128 位(实际上看起来比 256 位代码更好)。我没有立即找到可以生成 vextractf128 的版本。

标签: c++ optimization sse avx avx2


【解决方案1】:

您似乎正在对输出数组的每个元素进行水平求和。 (也许作为 matmul 的一部分?)这通常是次优的;尝试对第二个从内循环进行向量化,这样您就可以在向量中生成 result[i + 0..3] 而根本不需要水平总和。

对于一般的水平缩减,请参阅Fastest way to do horizontal SSE vector sum (or other reduction):提取高半部分并添加到低半部分。重复直到减少到 1 个元素。

如果您在内部循环中使用它,您肯定不想使用hadd(same,same)。除非您的编译器将您从自己手中拯救出来,否则这会花费 2 个 shuffle uops 而不是 1 个。 (而 gcc/clang 没有。)hadd 对代码大小很有用,但当您只有 1 个向量时几乎没有其他任何东西。通过两个不同的输入,它会很有用且高效。


对于 AVX,这意味着我们需要的唯一 256 位操作是提取,这在 AMD 和 Intel 上速度很快。那么剩下的都是128位的:

#include <immintrin.h>

inline
double hsum_double_avx(__m256d v) {
    __m128d vlow  = _mm256_castpd256_pd128(v);
    __m128d vhigh = _mm256_extractf128_pd(v, 1); // high 128
            vlow  = _mm_add_pd(vlow, vhigh);     // reduce down to 128

    __m128d high64 = _mm_unpackhi_pd(vlow, vlow);
    return  _mm_cvtsd_f64(_mm_add_sd(vlow, high64));  // reduce to scalar
}

如果您希望将结果广播到 __m256d 的每个元素,您可以使用 vshufpdvperm2f128 交换高/低半部分(如果针对 Intel 进行调整)。并使用 256-bit FP 加全时间。如果你完全关心早期的 Ryzen,你可能会减少到 128,使用_mm_shuffle_pd 进行交换,然后使用vinsertf128 获得 256 位向量。或者用AVX2,vbroadcastsd就这个最终结果。但这在 Intel 上会比一直保持 256 位同时仍然避免 vhaddpd 慢。

gcc7.3 -O3 -march=haswellon the Godbolt compiler explorer编译

    vmovapd         xmm1, xmm0               # silly compiler, vextract to xmm1 instead
    vextractf128    xmm0, ymm0, 0x1
    vaddpd          xmm0, xmm1, xmm0
    vunpckhpd       xmm1, xmm0, xmm0         # no wasted code bytes on an immediate for vpermilpd or vshufpd or anything
    vaddsd          xmm0, xmm0, xmm1         # scalar means we never raise FP exceptions for results we don't use
    vzeroupper
    ret

在内联之后(你肯定想要它),vzeroupper 沉入整个函数的底部,并希望 vmovapd 优化掉,vextractf128 进入不同的寄存器,而不是破坏保存 xmm0 _mm256_castpd256_pd128 结果。


在第一代 Ryzen (Zen 1 / 1+) 上,根据 Agner Fog's instruction tablesvextractf128 是 1 uop,延迟为 1c,吞吐量为 0.33c。

不幸的是,@PaulR 的版本在 Zen 2 之前的 AMD 上很糟糕;这就像您可能在英特尔库或编译器输出中找到的“跛脚 AMD”功能。 (我不认为 Paul 是故意这样做的,我只是指出忽略 AMD CPU 会导致代码在它们上运行得更慢。)

在 Zen 1 上,vperm2f128 是 8 uop、3c 延迟和每 3c 一个吞吐量。 vhaddpd ymm 是 8 uop(与您可能期望的 6 相比),7c 延迟,每 3c 吞吐量一个。 Agner 说这是一个“混合域”指令。而且 256 位操作总是至少需要 2 微秒。

     # Paul's version                      # Ryzen      # Skylake
    vhaddpd       ymm0, ymm0, ymm0         # 8 uops     # 3 uops
    vperm2f128    ymm1, ymm0, ymm0, 49     # 8 uops     # 1 uop
    vaddpd        ymm0, ymm0, ymm1         # 2 uops     # 1 uop
                           # total uops:   # 18         # 5

对比

     # my version with vmovapd optimized out: extract to a different reg
    vextractf128    xmm1, ymm0, 0x1        # 1 uop      # 1 uop
    vaddpd          xmm0, xmm1, xmm0       # 1 uop      # 1 uop
    vunpckhpd       xmm1, xmm0, xmm0       # 1 uop      # 1 uop
    vaddsd          xmm0, xmm0, xmm1       # 1 uop      # 1 uop
                           # total uops:   # 4          # 4

总 uop 吞吐量通常是混合了负载、存储和 ALU 的代码的瓶颈,因此我预计 4 uop 版本在 Intel 上可能至少要好一些,很多 AMD 更好。它还应该稍微减少热量,从而允许稍微更高的涡轮增压/使用更少的电池电量。 (但希望这个 hsum 是整个循环中足够小的一部分,可以忽略不计!)

延迟也不差,所以真的没有理由使用低效的hadd / vpermf128 版本。


Zen 2 及更高版本具有 256 位宽的向量寄存器和执行单元(包括 shuffle)。他们不必将车道交叉洗牌分成许多 uops,但相反,vextractf128 不再像vmovdqa xmm 那样便宜。 Zen 2 更接近英特尔的 256 位向量成本模型。

【讨论】:

    【解决方案2】:

    你可以这样做:

    acc = _mm256_hadd_pd(acc, acc);    // horizontal add top lane and bottom lane
    acc = _mm256_add_pd(acc, _mm256_permute2f128_pd(acc, acc, 0x31));  // add lanes
    result[i] = _mm256_cvtsd_f64(acc); // extract double
    

    注意:如果这是代码的“热”(即性能关键)部分(尤其是在 AMD CPU 上运行时),那么您可能希望查看 Peter Cordes's answer 以了解更有效的实现。

    【讨论】:

    • 在 Ryzen 上这是可怕,顺便说一句。 permutef128 比提取慢很多,并且没有理由为此执行任何 256 位向量操作,因为您需要单个标量结果并且不需要将结果广播到每个元素。在 Intel 上,唯一次优的部分是 hadd
    • 这正是我要找的!谢谢!
    • 啊 - 对 AMD CPU 一无所知 - 你有什么替代方案建议?
    • 我在 cmets 和 Fastest way to do horizontal float vector sum on x86 中所说的:提取到 128,然后按照 SSE2 方式进行 128 位 hsum:使用 shuffle + add。对英特尔来说并没有更糟,对 AMD 来说更好。根据 Agner Fog 的数据,vextractf128 在 AMD 上几乎是免费的:它甚至不是洗牌,因为向量已经在内部分成两半。
    • 是的,为了你自己的使用,如果你不使用它们,你不需要关心 AMD。但是对于 SO 答案,编写在 AMD 上也有效的代码绝对是一个好主意,尤其是当它在 Intel 上相同时。我认为,如果英特尔继续进行一些竞争,那么对行业来说会更健康,并且让随机的软件从 SO 答案中复制代码而不是在 AMD CPU 上让自己陷入困境有助于解决这个问题。如果他们的代码在更多的 CPU 上运行良好,复制您的代码的人可能会更开心:P 幸运的是,首先减少到 128 是好的并且容易记住。
    猜你喜欢
    • 2013-09-14
    • 2013-11-12
    • 1970-01-01
    • 2017-06-10
    • 2013-03-21
    • 1970-01-01
    • 1970-01-01
    • 2014-06-04
    • 2016-10-29
    相关资源
    最近更新 更多