使用SSE / AVX获取存储在__m256d中的值的总和

时间:2018-04-20 12:27:05

标签: c++ optimization sse avx avx2

有没有办法获得存储在__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指令替换它。

2 个答案:

答案 0 :(得分:7)

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

对于一般的水平缩减,请参阅Fastest way to do horizontal float vector sum on x86:提取高半部分并添加到低半部分。重复,直到你归结为1个元素。

如果您在内循环中使用此功能,那么您绝对不想使用hadd(same,same)。除非您的编译器将您从自己身上拯救出来,否则这需要花费2个shuffle uops而不是1。 (并且gcc / clang不要。)hadd对代码大小有好处,但几乎没有别的,除非你可以将它用于两个不同的输入。

对于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
}

如果您希望将结果广播到__m256的每个元素,您可以使用vshufpdvperm2f128来交换高/低一半 (如果调整为英特尔)。并使用256位FP添加整个时间。如果您完全关心Ryzen,可以减少到128,使用_mm_shuffle_pd进行交换,然后使用vinsertf128获得256位向量。或者使用AVX2,vbroadcastsd得出最终结果。但是,对于英特尔而言,这一点要比在整个时间内保持256位的速度慢,同时仍然避免使用vhaddpd

使用gcc7.3 -O3 -march=haswell on 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改为不同的寄存器破坏保存_mm256_castpd256_pd128结果的xmm0。

根据Agner Fog's instruction tables,在Ryzen上,vextractf128是1 uop,延迟为1c,吞吐量为0.33c。

不幸的是,@ PaulR&#39的版本在AMD上很糟糕;它就像你可能在英特尔库或编译器输出中找到的东西,作为一个&#34;瘫痪AMD&#34;功能。 (我不认为Paul故意这样做,我只是指出忽略AMD CPU会导致代码运行速度变慢。)

在Ryzen上,vperm2f128是8 uops,3c延迟,每3c吞吐量一个。 vhaddpd ymm是8 uops(相对于你可能预期的6),7c延迟,每3c吞吐量一个。阿格纳说,它是一个混合领域&#34;指令。 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

VS

     # 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版本在英特尔上可能至少要好一些,以及更好的AMD。它也应该稍微减少热量,因此允许稍高的涡轮/使用更少的电池电量。 (但希望这个hsum是你的总循环中的一小部分,这可以忽略不计!)

延迟也不会更糟,因此我们没有理由使用效率低下的hadd / vpermf128版本。

答案 1 :(得分:3)

你可以这样做:

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