获得 8 个源 __m256 向量的水平和的 __m256 的最有效方法
Most efficient way to get a __m256 of horizontal sums of 8 source __m256 vectors
我知道如何对 __m256
求和以获得单个求和值。但是,我有 8 个向量,例如
输入
1: a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7],
.....,
.....,
8: h[0], h[1], h[2], h[3], h[4], a[5], a[6], a[7]
输出
a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7],
....,
h[0]+h[1]+h[2]+h[3]+h[4]+h[5]+h[6]+h[7]
我的方法。好奇有没有更好的方法。
__m256 sumab = _mm256_hadd_ps(accumulator1, accumulator2);
__m256 sumcd = _mm256_hadd_ps(accumulator3, accumulator4);
__m256 sumef = _mm256_hadd_ps(accumulator5, accumulator6);
__m256 sumgh = _mm256_hadd_ps(accumulator7, accumulator8);
__m256 sumabcd = _mm256_hadd_ps(sumab, sumcd);
__m256 sumefgh = _mm256_hadd_ps(sumef, sumgh);
__m128 sumabcd1 = _mm256_extractf128_ps(sumabcd, 0);
__m128 sumabcd2 = _mm256_extractf128_ps(sumabcd, 1);
__m128 sumefgh1 = _mm256_extractf128_ps(sumefgh, 0);
__m128 sumefgh2 = _mm256_extractf128_ps(sumefgh, 1);
sumabcd1 = _mm_add_ps(sumabcd1, sumabcd2);
sumefgh1 = _mm_add_ps(sumefgh1, sumefgh2);
__m256 result =_mm256_insertf128_ps(_mm256_castps128_ps256(sumabcd1), sumefgh1, 1)
更新: 是(我认为)同样的问题,用一个混合替换 _mm256_permute2f128_ps 中的一个解决了。另一个答案是用更多的混合代替洗牌 uops。请改用其中之一。
原始答案未能使用任何混合并且会在随机播放时出现瓶颈
您可以使用 2x _mm256_permute2f128_ps
将低车道和高车道排列成垂直 vaddps
。这不是 2x extractf128
/ insertf128
。这也将两个 128b vaddps xmm
指令变成一个 256b vaddps ymm
.
vperm2f128
与英特尔 CPU 上的单个 vextractf128
或 vinsertf128
一样快。不过,它在 AMD 上很慢(在 Bulldozer 系列上有 4c 延迟的 8 m-ops)。尽管如此,即使您关心 AMD 的性能,也还不错,您需要避免使用它。 (其中一个排列实际上可以是 vinsertf128
)。
__m256 hsum8(__m256 a, __m256 b, __m256 c, __m256 d,
__m256 e, __m256 f, __m256 g, __m256 h)
{
// a = [ A7 A6 A5 A4 | A3 A2 A1 A0 ]
__m256 sumab = _mm256_hadd_ps(a, b);
__m256 sumcd = _mm256_hadd_ps(c, d);
__m256 sumef = _mm256_hadd_ps(e, f);
__m256 sumgh = _mm256_hadd_ps(g, h);
__m256 sumabcd = _mm256_hadd_ps(sumab, sumcd); // [ D7:4 ... A7:4 | D3:0 ... A3:0 ]
__m256 sumefgh = _mm256_hadd_ps(sumef, sumgh); // [ H7:4 ... E7:4 | H3:0 ... E3:0 ]
__m256 sum_hi = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x31); // [ H7:4 ... E7:4 | D7:4 ... A7:4 ]
__m256 sum_lo = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x20); // [ H3:0 ... E3:0 | D3:0 ... A3:0 ]
__m256 result = _mm256_add_ps(sum_hi, sum_lo);
return result;
}
这个compiles as you'd expect。第二个 permute2f128
实际上编译为 vinsertf128
,因为它仅以与 vinsertf128
相同的方式使用每个输入的低通道。 gcc 4.7 及更高版本进行了此优化,但只有更新的 clang 版本 (v3.7) 进行了此优化。如果您关心旧的 clang,请在源代码级别执行此操作。
源代码行的节省大于指令的节省,因为 _mm256_extractf128_ps(sumabcd, 0);
编译为零指令:它只是一个转换。除了 1
之外,任何编译器都不应该使用 imm8 发出 vextractf128
。 (vmovdqa xmm/m128, xmm
总是更适合进入低车道)。 Intel 干得好,它在你无法使用的未来验证上浪费了一个指令字节,因为普通的 VEX 前缀没有空间来编码更长的向量。
两个 vaddps xmm
指令可以 运行 并行,因此使用单个 vaddps ymm
主要只是吞吐量(和代码大小)增益,而不是延迟。
不过,我们确实从完全消除最后的 vinsertf128
中减少了 3 个周期。
vhaddps
是 3 微指令,5c 延迟,每 2c 吞吐量一个。 (Skylake 上的 6c 延迟)。 shuffle 端口上的这三个微指令中的两个 运行。我想它基本上是在做 2x shufps
来为 addps
.
生成操作数
如果我们可以用单个 shufps
/addps
或其他东西来模拟 haddps
(或者至少获得我们可以使用的水平操作),我们就会领先。不幸的是,我不知道如何。一次随机播放只能使用来自两个向量的数据产生一个结果,但是我们需要垂直输入 addps
才能获得来自两个向量的数据。
我不认为用另一种方式进行水平求和看起来很有希望。 Normally, hadd is not a good choice,因为常见的水平和用例只关心其输出的一个元素。这里不是这种情况:每个 hadd
结果的每个元素实际上都被使用了。
我知道如何对 __m256
求和以获得单个求和值。但是,我有 8 个向量,例如
输入
1: a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7],
.....,
.....,
8: h[0], h[1], h[2], h[3], h[4], a[5], a[6], a[7]
输出
a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7],
....,
h[0]+h[1]+h[2]+h[3]+h[4]+h[5]+h[6]+h[7]
我的方法。好奇有没有更好的方法。
__m256 sumab = _mm256_hadd_ps(accumulator1, accumulator2);
__m256 sumcd = _mm256_hadd_ps(accumulator3, accumulator4);
__m256 sumef = _mm256_hadd_ps(accumulator5, accumulator6);
__m256 sumgh = _mm256_hadd_ps(accumulator7, accumulator8);
__m256 sumabcd = _mm256_hadd_ps(sumab, sumcd);
__m256 sumefgh = _mm256_hadd_ps(sumef, sumgh);
__m128 sumabcd1 = _mm256_extractf128_ps(sumabcd, 0);
__m128 sumabcd2 = _mm256_extractf128_ps(sumabcd, 1);
__m128 sumefgh1 = _mm256_extractf128_ps(sumefgh, 0);
__m128 sumefgh2 = _mm256_extractf128_ps(sumefgh, 1);
sumabcd1 = _mm_add_ps(sumabcd1, sumabcd2);
sumefgh1 = _mm_add_ps(sumefgh1, sumefgh2);
__m256 result =_mm256_insertf128_ps(_mm256_castps128_ps256(sumabcd1), sumefgh1, 1)
更新:
原始答案未能使用任何混合并且会在随机播放时出现瓶颈
您可以使用 2x _mm256_permute2f128_ps
将低车道和高车道排列成垂直 vaddps
。这不是 2x extractf128
/ insertf128
。这也将两个 128b vaddps xmm
指令变成一个 256b vaddps ymm
.
vperm2f128
与英特尔 CPU 上的单个 vextractf128
或 vinsertf128
一样快。不过,它在 AMD 上很慢(在 Bulldozer 系列上有 4c 延迟的 8 m-ops)。尽管如此,即使您关心 AMD 的性能,也还不错,您需要避免使用它。 (其中一个排列实际上可以是 vinsertf128
)。
__m256 hsum8(__m256 a, __m256 b, __m256 c, __m256 d,
__m256 e, __m256 f, __m256 g, __m256 h)
{
// a = [ A7 A6 A5 A4 | A3 A2 A1 A0 ]
__m256 sumab = _mm256_hadd_ps(a, b);
__m256 sumcd = _mm256_hadd_ps(c, d);
__m256 sumef = _mm256_hadd_ps(e, f);
__m256 sumgh = _mm256_hadd_ps(g, h);
__m256 sumabcd = _mm256_hadd_ps(sumab, sumcd); // [ D7:4 ... A7:4 | D3:0 ... A3:0 ]
__m256 sumefgh = _mm256_hadd_ps(sumef, sumgh); // [ H7:4 ... E7:4 | H3:0 ... E3:0 ]
__m256 sum_hi = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x31); // [ H7:4 ... E7:4 | D7:4 ... A7:4 ]
__m256 sum_lo = _mm256_permute2f128_ps(sumabcd, sumefgh, 0x20); // [ H3:0 ... E3:0 | D3:0 ... A3:0 ]
__m256 result = _mm256_add_ps(sum_hi, sum_lo);
return result;
}
这个compiles as you'd expect。第二个 permute2f128
实际上编译为 vinsertf128
,因为它仅以与 vinsertf128
相同的方式使用每个输入的低通道。 gcc 4.7 及更高版本进行了此优化,但只有更新的 clang 版本 (v3.7) 进行了此优化。如果您关心旧的 clang,请在源代码级别执行此操作。
源代码行的节省大于指令的节省,因为 _mm256_extractf128_ps(sumabcd, 0);
编译为零指令:它只是一个转换。除了 1
之外,任何编译器都不应该使用 imm8 发出 vextractf128
。 (vmovdqa xmm/m128, xmm
总是更适合进入低车道)。 Intel 干得好,它在你无法使用的未来验证上浪费了一个指令字节,因为普通的 VEX 前缀没有空间来编码更长的向量。
两个 vaddps xmm
指令可以 运行 并行,因此使用单个 vaddps ymm
主要只是吞吐量(和代码大小)增益,而不是延迟。
不过,我们确实从完全消除最后的 vinsertf128
中减少了 3 个周期。
vhaddps
是 3 微指令,5c 延迟,每 2c 吞吐量一个。 (Skylake 上的 6c 延迟)。 shuffle 端口上的这三个微指令中的两个 运行。我想它基本上是在做 2x shufps
来为 addps
.
如果我们可以用单个 shufps
/addps
或其他东西来模拟 haddps
(或者至少获得我们可以使用的水平操作),我们就会领先。不幸的是,我不知道如何。一次随机播放只能使用来自两个向量的数据产生一个结果,但是我们需要垂直输入 addps
才能获得来自两个向量的数据。
我不认为用另一种方式进行水平求和看起来很有希望。 Normally, hadd is not a good choice,因为常见的水平和用例只关心其输出的一个元素。这里不是这种情况:每个 hadd
结果的每个元素实际上都被使用了。