Update: 计算 8 个 AVX 单精度浮点向量的 8 个水平和(我认为)是同样的问题,用一个混合替换其中一个 _mm256_permute2f128_ps 来解决。另一个答案是用更多混合代替洗牌微指令。请改用其中之一。
原始答案未能使用任何混合,并且会在洗牌上出现瓶颈
您可以使用 2x_mm256_permute2f128_ps
将低车道和高车道对齐以实现垂直vaddps
。这不是 2xextractf128
/ insertf128
。这也变成了两个128bvaddps xmm
指令写入单个 256bvaddps ymm
.
vperm2f128
与单个一样快vextractf128
or vinsertf128
在英特尔 CPU 上。不过,在 AMD 上速度很慢(Bulldozer 系列上有 8 m-ops,延迟为 4c)。不过,即使您关心 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;
}
This 按您的预期编译。第二permute2f128
实际上编译为vinsertf128
,因为它仅以与vinsertf128
做。 gcc 4.7 及更高版本执行此优化,但只有更新的 clang 版本才会执行此优化 (v3.7)。如果您关心旧的 clang,请在源代码级别执行此操作。
源代码行的节省大于指令的节省,因为_mm256_extractf128_ps(sumabcd, 0);
编译为零指令:这只是一个强制转换。任何编译器都不应该发出vextractf128
与 imm8 以外的1
. (vmovdqa xmm/m128, xmm
总是更适合进入低车道)。干得好,英特尔浪费了一个指令字节来保证您无法使用,因为普通的 VEX 前缀没有空间来编码更长的向量。
The two vaddps xmm
指令可以并行运行,因此使用单个vaddps ymm
主要只是吞吐量(和代码大小)增益,而不是延迟。
我们确实缩短了 3 个周期,从而完全消除了最终的结果vinsertf128
, 尽管。
vhaddps
是 3 uop,5c 延迟,每 2c 吞吐量 1 个。 (Skylake 上的延迟为 6c)。这三个微指令中的两个在随机端口上运行。我猜它基本上是做 2xshufps
生成操作数addps
.
如果我们可以效仿haddps
(或者至少获得我们可以使用的水平操作)shufps
/addps
或者其他什么,我们会领先。不幸的是,我不知道如何。一次洗牌只能用来自两个向量的数据产生一个结果,但我们需要垂直输入addps
从两个向量中获取数据。
我认为以另一种方式进行水平求和看起来没有希望。通常,哈德不是一个好的选择,因为常见的水平求和用例只关心其输出的一个元素。这里的情况并非如此:每个元素的每个元素hadd
实际使用的结果。