您的缩减循环既是延迟瓶颈,又不是吞吐量瓶颈,因为您只使用一个 FP 向量累加器。 FMA 速度较慢,因为您使关键路径更长(每个循环迭代有 2 条指令链,而不是 1 条指令)。
In the add
在这种情况下,循环携带依赖链sum
只是sum=_mm256_add_ps(sum, abcdm);
。其他指令对于每次迭代都是独立的,并且可以具有abcdm
输入准备好前一个vaddps
有这个迭代的sum
ready.
In the fma
在这种情况下,循环携带的 dep 链经过两个_mm256_fmadd_ps
操作,都进入sum
,所以是的,您预计它会慢两倍左右。
使用更多累加器展开以隐藏 FP 延迟(就像点积的正常情况一样). See 为什么mulss在Haswell上只需要3个周期,与Agner的指令表不同? (使用多个累加器展开 FP 循环) https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction有关此内容以及 OoO exec 工作原理的更多详细信息。
另请参阅使用 SIMD 提高数组浮点点积的性能 https://stackoverflow.com/questions/65818232/improving-performance-of-floating-point-dot-product-of-an-array-with-simd/65827668#65827668这是一个更简单、适合初学者的 2 个累加器示例。
(将这些单独的__m256 sum0, sum1, sum2, etc
vars 应该在循环之后完成。您还可以使用__m256 sum[4]
以节省打字。您甚至可以对该数组使用内部循环;大多数编译器将完全展开小型固定计数循环,因此您可以在每个循环中获得所需的展开汇编__m256
在单独的 YMM 寄存器中。)
或者让 clang 自动矢量化它;它通常会为您使用多个累加器展开。
或者,如果您出于某种原因不想展开,您可以使用 FMA,同时使用以下命令保持较低的循环承载延迟:sum += fma(a, b, c*d);
(一份 mul、一份 FMA、一份添加)。当然,假设你的编译器没有“收缩”你的 mul 并为你添加到 FMA 中,如果你使用-ffast-math
;默认情况下,GCC 会在语句中积极执行此操作,但 clang 不会。
一旦你这样做了,你的吞吐量将在每个时钟 2 个负载上成为瓶颈(最好的情况是即使使用对齐阵列,也没有缓存行分割,这new
won't给你),所以使用 FMA 除了减少前端瓶颈之外几乎没有帮助。 (与需要在每个负载运行 1 FP 操作才能跟上的多累加器 mul/add 版本相比;使用多个累加器将使您比任一原始循环更快。就像每 2 个周期进行一次迭代(4 个负载),而不是 1每 3 个周期vaddps
延迟瓶颈)。
在 Skylake 及更高版本上,FMA/add/mul 都具有相同的延迟:4 个周期。在 Haswell/Broadwell 上,vaddps 延迟为 3 个周期(一个专用 FP 添加单元),而 FMA 延迟为 5。
Zen2 有 3 个周期 vaddps、5 个周期 vfma....ps (https://uops.info/ https://uops.info/)。 (两者的 2/时钟吞吐量,并且在不同的执行端口上,因此理论上您可以运行 2 个 FMAandZen2 上每个时钟 2 个 vaddp。)
由于您的较长延迟 FMA 循环的速度不到两倍,我猜测您可能使用的是 Skylake 衍生的 CPU。也许 mul/add 版本在前端或资源冲突或其他方面遇到了一些瓶颈,并且没有完全达到预期的每 3 个时钟 1 次迭代延迟限制速度。
一般来说,请参阅https://uops.info/ https://uops.info/用于延迟和微指令/端口故障。
(还https://agner.org/optimize/ https://agner.org/optimize/).