使用 AVX 一次性完成 4 个水平双精度求和

Posted

技术标签:

【中文标题】使用 AVX 一次性完成 4 个水平双精度求和【英文标题】:4 horizontal double-precision sums in one go with AVX 【发布时间】:2012-05-31 12:03:13 【问题描述】:

问题可以描述如下。

输入

__m256d a, b, c, d

输出

__m256d s = a[0]+a[1]+a[2]+a[3], b[0]+b[1]+b[2]+b[3], 
             c[0]+c[1]+c[2]+c[3], d[0]+d[1]+d[2]+d[3]

到目前为止我所做的工作

这似乎很容易:两个 VHADD 之间有一些改组,但实际上结合 AVX 的所有排列不能生成实现该目标所需的排列。让我解释一下:

VHADD x, a, b => x = a[0]+a[1], b[0]+b[1], a[2]+a[3], b[2]+b[3]
VHADD y, c, d => y = c[0]+c[1], d[0]+d[1], c[2]+c[3], d[2]+d[3]

我是否能够以相同的方式排列 x 和 y 以获得

x1 = a[0]+a[1], a[2]+a[3], c[0]+c[1], c[2]+c[3]
y1 = b[0]+b[1], b[2]+b[3], d[0]+d[1], d[2]+d[3]

然后

VHADD s, x1, y1 => s1 = a[0]+a[1]+a[2]+a[3], b[0]+b[1]+b[2]+b[3], 
                         c[0]+c[1]+c[2]+c[3], d[0]+d[1]+d[2]+d[3]

这是我想要的结果。

所以我只需要找到如何执行

x,y => x[0], x[2], y[0], y[2], x[1], x[3], y[1], y[3]

不幸的是,我得出的结论是,使用 VSHUFPD、VBLENDPD、VPERMILPD、VPERM2F128、VUNPCKHPD、VUNPCKLPD 的任何组合都证明是不可能的。问题的关键在于,在__m256d的实例u中,u[1]和u[2]是不可能交换的。

问题

这真的是死胡同吗?还是我错过了排列指令?

【问题讨论】:

您可以通过在输入中交换 bc 来简化任务。这样,您只需交换内部的两个双打。无论如何,这可以通过使用vperm2f128、两个备用寄存器、vpermilps 交换第一个/第二个和第三个/第四个以及最后一个vunpack[hl]pd 组合来完成粗心的源代码阅读器的组合来实现。 另一个想法(可能更快)是输入矩阵的转置(沿对角线镜像)并进行垂直求和。即转化为a0, b0, c0, d0等,简单加起来。 我也想过交换 b 和 c。由于这段代码可能会被我以外的其他人重用,我希望我能找到一个没有这种扭曲的解决方案!至于您的第二个建议,我无法重新组织代码以首先生成转置矩阵,并且我看不到执行转置的有效方法,所以我会说死路一条。 【参考方案1】:

VHADD 指令后面是常规的VADD。下面的代码应该给你你想要的:

// a[0]+a[1], b[0]+b[1], a[2]+a[3], b[2]+b[3]
__m256d sumab = _mm256_hadd_pd(a, b);
// c[0]+c[1], d[0]+d[1], c[2]+c[3], d[2]+d[3]
__m256d sumcd = _mm256_hadd_pd(c, d);

// a[0]+a[1], b[0]+b[1], c[2]+c[3], d[2]+d[3]
__m256d blend = _mm256_blend_pd(sumab, sumcd, 0b1100);
// a[2]+a[3], b[2]+b[3], c[0]+c[1], d[0]+d[1]
__m256d perm = _mm256_permute2f128_pd(sumab, sumcd, 0x21);

__m256d sum =  _mm256_add_pd(perm, blend);

这给出了 5 条指令的结果。我希望我得到了正确的常量。

您提出的排列当然可以完成,但需要多条指令。抱歉,我没有回答你的那部分问题。

编辑:我无法抗拒,这是完整的排列。 (再一次,尽我最大的努力使常量正确。)您可以看到交换u[1]u[2] 是可能的,只需要一些工作。在第一代中跨越 128 位的障碍是很困难的。 AVX。我还想说VADDVHADD 更可取,因为VADD 具有两倍的吞吐量,即使它执行相同数量的添加。

// x[0],x[1],x[2],x[3]
__m256d x;

// x[1],x[0],x[3],x[2]
__m256d xswap = _mm256_permute_pd(x, 0b0101);

// x[3],x[2],x[1],x[0]
__m256d xflip128 = _mm256_permute2f128_pd(xswap, xswap, 0x01);

// x[0],x[2],x[1],x[3] -- not imposssible to swap x[1] and x[2]
__m256d xblend = _mm256_blend_pd(x, xflip128, 0b0110);

// repeat the same for y
// y[0],y[2],y[1],y[3]
__m256d yblend;

// x[0],x[2],y[0],y[2]
__m256d x02y02 = _mm256_permute2f128_pd(xblend, yblend, 0x20);

// x[1],x[3],y[1],y[3]
__m256d x13y13 = _mm256_permute2f128_pd(xblend, yblend, 0x31);

【讨论】:

聪明!谢谢!不,我向您保证,我提出的排列无法完成,但您的方案使其成为多余。并且通过 5 条指令,您给出了最有效的解决方案。 @ljbou:在当前一代的 AVX 中,最多可以在 4 条指令中执行 __m256d 的任意置换:一条 VPERM2F128,两条 VSHUFPD,和一条VBLENDPD。传入的AVX2 in Haswell 功能更强大,允许在一条指令中进行任意排列(我认为是VPERMPD)。 这真是个天才,在水平添加上挣扎了几个小时。您是否知道任何以尽可能少的指令/周期实现这些常用操作的库?【参考方案2】:

我不知道有任何指令可以让您进行这种排列。 AVX 指令的操作通常使得寄存器的高 128 位和低 128 位有些独立;将两半的值混合的能力并不多。我能想到的最佳实现将基于对this question 的回答:

__m128d horizontal_add_pd(__m256d x1, __m256d x2)

    // calculate 4 two-element horizontal sums:
    // lower 64 bits contain x1[0] + x1[1]
    // next 64 bits contain x2[0] + x1[1]
    // next 64 bits contain x1[2] + x1[3]
    // next 64 bits contain x2[2] + x2[3]
    __m256d sum = _mm256_hadd_pd(x1, x2);
    // extract upper 128 bits of result
    __m128d sum_high = _mm256_extractf128_pd(sum1, 1);
    // add upper 128 bits of sum to its lower 128 bits
    __m128d result = _mm_add_pd(sum_high, (__m128d) sum);
    // lower 64 bits of result contain the sum of x1[0], x1[1], x1[2], x1[3]
    // upper 64 bits of result contain the sum of x2[0], x2[1], x2[2], x2[3]
    return result;


__m256d a, b, c, d;
__m128d res1 = horizontal_add_pd(a, b);
__m128d res2 = horizontal_add_pd(c, d);
// At this point:
//     res1 contains a's horizontal sum in bits 0-63
//     res1 contains b's horizontal sum in bits 64-127
//     res2 contains c's horizontal sum in bits 0-63
//     res2 contains d's horizontal sum in bits 64-127
// cast res1 to a __m256d, then insert res2 into the upper 128 bits of the result
__m256d sum = _mm256_insertf128_pd(_mm256_castpd128_pd256(res1), res2, 1);
// At this point:
//     sum contains a's horizontal sum in bits 0-63
//     sum contains b's horizontal sum in bits 64-127
//     sum contains c's horizontal sum in bits 128-191
//     sum contains d's horizontal sum in bits 192-255

这应该是你想要的。以上应该在 7 条指令中是可行的(强制转换不应该真正做任何事情;它只是给编译器的一个注释,以改变它处理 res1 中的值的方式),假设短 horizontal_add_pd() 函数可以是由您的编译器内联,并且您有足够的可用寄存器。

【讨论】:

正如 drhirsch 指出的那样,您可以将其视为相当于 4 个垂直总和加上一些开销,您的解决方案相当于 3/4,而 Norbert 的解决方案下降到 1/4。 在所有支持它的 Intel 和 AMD CPU 上都已解码为 3 uop(2x shuffle 馈送垂直添加)。不值得在两个输入相同的情况下使用,除非您正在优化代码大小而不是速度。提取高半部分并添加到低半部分,从 256 下降到 128,然后从 128 下降到 64(例如 vunpcklpd)。 请参阅Get sum of values stored in __m256d with SSE/AVX 了解编译为高效 asm 的方法。 (但这些都没有回答这个问题,这不是关于一个寄存器的 hsum,而是关于 sum+transpose,_mm256_hadd_pd 的少数用例之一。)

以上是关于使用 AVX 一次性完成 4 个水平双精度求和的主要内容,如果未能解决你的问题,请参考以下文章

加速图像处理的神器: INTEL ISPC 编译器迁移图像旋转算法 - 从 ISPC双精度到 ISPC单精度

加速图像处理的神器: INTEL ISPC 编译器迁移图像旋转算法 - 从 ISPC双精度到 ISPC单精度

加速图像处理的神器: INTEL ISPC 编译器迁移图像旋转算法 - 从 ISPC双精度到 ISPC单精度

AVX计算精度

我在哪里可以找到 AVX 指数双精度函数?

开发新的指令集