对除法要非常小心,并尽可能避免它。例如,将float inverse = 1.0f / divisor; 提升出循环并在循环内乘以inverse。 (如果inverse的舍入误差可以接受)
通常1.0/x 不能精确地表示为float 或double。当 x 是 2 的幂时将是精确的。这让编译器可以将 x / 2.0f 优化为 x * 0.5f,而不会改变结果。
即使结果不准确(或使用运行时变量除数),为了让编译器为您执行此优化,您需要像 gcc -O3 -ffast-math 这样的选项。具体来说,-freciprocal-math(由-funsafe-math-optimizations 启用,由-ffast-math 启用)允许编译器在有用时将x / y 替换为x * (1/y)。其他编译器也有类似的选项,ICC 可能会默认启用一些“不安全”的优化(我认为是,但我忘记了)。
-ffast-math 对于允许 FP 循环的自动矢量化通常很重要,尤其是减少(例如,将一个数组求和为一个标量总数),因为 FP 数学不是关联的。 Why doesn't GCC optimize a*a*a*a*a*a to (a*a*a)*(a*a*a)?
另请注意,C++ 编译器可以在某些情况下将+ 和* 折叠到FMA 中(当为支持它的目标进行编译时,例如-march=haswell),但它们不能用/ 做到这一点.
在现代 x86 CPU 上,除法的延迟比乘法或加法(或 FMA)差 2 到 4 倍,吞吐量差 6 到 40 倍1(对于一个紧密的循环,only 除法而不是 only 乘法)。
divide / sqrt 单元未完全流水线化,原因在@NathanWhitehead's answer 中解释。最差的比率是 256b 向量,因为(与其他执行单元不同)除法单元通常不是全角的,因此宽向量必须分成两半。一个不完全流水线的执行单元是如此的不寻常,以至于英特尔 CPU 有一个arith.divider_active 硬件性能计数器来帮助您找到对除法器吞吐量造成瓶颈的代码,而不是通常的前端或执行端口瓶颈。 (或者更常见的是,内存瓶颈或长延迟链限制了指令级并行性,导致指令吞吐量低于每个时钟约 4 个)。
但是,英特尔和 AMD CPU(KNL 除外)上的 FP 除法和 sqrt 是作为单个 uop 实现的,因此它不一定对周围代码产生很大的吞吐量影响。除法的最佳情况是乱序执行可以隐藏延迟,并且当除法可能并行发生大量乘法和加法(或其他工作)时。
(整数除法在Intel上被微编码为多个微指令,因此它总是对整数相乘的周围代码产生更大的影响。对高性能整数除法的需求较少,因此硬件支持并不那么花哨。相关:@ 987654325@.)
例如,这将是非常糟糕的:
for ()
a[i] = b[i] / scale; // division throughput bottleneck
// Instead, use this:
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv; // multiply (or store) throughput bottleneck
您在循环中所做的只是加载/分割/存储,它们是独立的,因此重要的是吞吐量,而不是延迟。
像accumulator /= b[i] 这样的减少会成为除法或乘法延迟的瓶颈,而不是吞吐量。但是使用最后除法或乘法的多个累加器,您可以隐藏延迟并仍然使吞吐量饱和。请注意,sum += a[i] / b[i] 是 add 延迟或 div 吞吐量的瓶颈,但不是 div 延迟,因为该划分不在关键路径(循环承载的依赖链)上。
但在这样的情况下 (approximating a function like log(x) with a ratio of two polynomials),分水岭可能相当便宜:
for () {
// (not shown: extracting the exponent / mantissa)
float p = polynomial(b[i], 1.23, -4.56, ...); // FMA chain for a polynomial
float q = polynomial(b[i], 3.21, -6.54, ...);
a[i] = p/q;
}
对于在尾数范围内的log(),两个 N 阶多项式的比率比具有 2N 个系数的单个多项式的误差要小得多,并且并行计算 2 可以在单个循环内提供一些指令级并行性body 而不是一个巨大的长 dep 链,使乱序执行变得容易得多。
在这种情况下,我们不会成为除法延迟的瓶颈,因为乱序执行可以使循环在数组上的多次迭代保持在运行中。
只要我们的多项式足够大,以至于每 10 条左右的 FMA 指令只有一个除法,我们就不会在除法上遇到瓶颈吞吐量。 (在真实的log() 用例中,提取指数/尾数并将事物重新组合在一起需要做大量工作,因此在除法之间还有更多工作要做。)
当你确实需要分割时,通常最好只分割而不是rcpps
x86 有一个近似倒数指令 (rcpps),它只给你 12 位的精度。 (AVX512F有14位,AVX512ER有28位。)
您可以使用它来执行x / y = x * approx_recip(y),而无需使用实际的除法指令。 (rcpps itsef 相当快;通常比乘法慢一点。它使用从 CPU 内部表中查找表。除法器硬件可能使用同一个表作为起点。)
在大多数情况下,x * rcpps(y) 太不准确,需要 Newton-Raphson 迭代以使精度加倍。但这会花费您2 multiplies and 2 FMAs,并且延迟大约与实际除法指令一样高。如果 all 你正在做的是除法,那么它可以是吞吐量的胜利。 (但如果可以的话,你应该首先避免这种循环,也许通过将除法作为另一个循环的一部分来完成其他工作。)
但是,如果您将除法用作更复杂函数的一部分,rcpps 本身 + 额外的 mul + FMA 通常可以更快地使用 divps 指令进行除法,除非在 @ 非常低的 CPU 上987654372@ 吞吐量。
(例如 Knight's Landing,见下文。KNL 支持 AVX512ER,因此对于 float 向量,VRCP28PS 结果已经足够准确,无需 Newton-Raphson 迭代即可相乘。float 尾数大小仅24 位。)
Agner Fog 表格中的具体数字:
与其他所有 ALU 操作不同,除法延迟/吞吐量取决于某些 CPU 的数据。同样,这是因为它太慢并且没有完全流水线化。无序调度在固定延迟的情况下更容易,因为它避免了回写冲突(当同一个执行端口试图在同一个周期内产生 2 个结果时,例如运行 3 个周期的指令,然后运行两个 1 个周期的操作) .
通常,最快的情况是除数是“整数”,如 2.0 或 0.5(即 base2 float 表示在尾数中有很多尾随零)。
float 延迟(周期)/吞吐量(每条指令的周期,使用独立输入背靠背运行):
scalar & 128b vector 256b AVX vector
divss | mulss
divps xmm | mulps vdivps ymm | vmulps ymm
Nehalem 7-14 / 7-14 | 5 / 1 (No AVX)
Sandybridge 10-14 / 10-14 | 5 / 1 21-29 / 20-28 (3 uops) | 5 / 1
Haswell 10-13 / 7 | 5 / 0.5 18-21 / 14 (3 uops) | 5 / 0.5
Skylake 11 / 3 | 4 / 0.5 11 / 5 (1 uop) | 4 / 0.5
Piledriver 9-24 / 5-10 | 5-6 / 0.5 9-24 / 9-20 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 10 / 3 | 3 / 0.5 10 / 6 (2 uops) | 3 / 1 (2 uops)
Low-power CPUs:
Jaguar(scalar) 14 / 14 | 2 / 1
Jaguar 19 / 19 | 2 / 1 38 / 38 (2 uops) | 2 / 2 (2 uops)
Silvermont(scalar) 19 / 17 | 4 / 1
Silvermont 39 / 39 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 27 / 17 (3 uops) | 6 / 0.5
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
double 延迟(周期)/吞吐量(每条指令的周期):
scalar & 128b vector 256b AVX vector
divsd | mulsd
divpd xmm | mulpd vdivpd ymm | vmulpd ymm
Nehalem 7-22 / 7-22 | 5 / 1 (No AVX)
Sandybridge 10-22 / 10-22 | 5 / 1 21-45 / 20-44 (3 uops) | 5 / 1
Haswell 10-20 / 8-14 | 5 / 0.5 19-35 / 16-28 (3 uops) | 5 / 0.5
Skylake 13-14 / 4 | 4 / 0.5 13-14 / 8 (1 uop) | 4 / 0.5
Piledriver 9-27 / 5-10 | 5-6 / 1 9-27 / 9-18 (2 uops) | 5-6 / 1 (2 uops)
Ryzen 8-13 / 4-5 | 4 / 0.5 8-13 / 8-9 (2 uops) | 4 / 1 (2 uops)
Low power CPUs:
Jaguar 19 / 19 | 4 / 2 38 / 38 (2 uops) | 4 / 2 (2 uops)
Silvermont(scalar) 34 / 32 | 5 / 2
Silvermont 69 / 69 (6 uops) | 5 / 2 (No AVX)
KNL(scalar) 42 / 42 (3 uops) | 6 / 0.5 (Yes, Agner really lists scalar as slower than packed, but fewer uops)
KNL 32 / 20 (18uops) | 6 / 0.5 32 / 32 (18 uops) | 6 / 0.5 (AVX and AVX512)
Ivybridge 和 Broadwell 也不同,但我想保持桌子小。 (Core2(Nehalem 之前)具有更好的分频器性能,但其最大时钟速度较低。)
Atom、Silvermont 和 甚至 Knight's Landing(基于 Silvermont 的 Xeon Phi)都具有极低的除法性能,甚至 128b 向量也比标量慢。 AMD 的低功耗 Jaguar CPU(用于某些游戏机)类似。高性能分频器占用大量芯片面积。 Xeon Phi 具有 每核 低功耗,并且在一个裸片上封装大量核心使其在裸片面积上的限制比 Skylake-AVX512 更严格。看来 AVX512ER rcp28ps / pd 是您“应该”在 KNL 上使用的。
(有关 Skylake-AVX512 aka Skylake-X,请参见 this InstLatx64 result。vdivps zmm 的数字:18c / 10c,因此吞吐量是 ymm 的一半。)
当它们被循环携带时,或者当它们太长以至于它们停止乱序执行以寻找与其他独立工作的并行性时,长延迟链会成为一个问题。
脚注 1:我是如何计算这些 div 与 mul 性能比的:
FP 分频与倍数性能比甚至比 Silvermont 和 Jaguar 等低功耗 CPU 甚至在 Xeon Phi(KNL,您应该使用 AVX512ER)中更差。
标量(非矢量化)double 的实际除法/乘法吞吐量比:在 Ryzen 和 Skylake 上使用增强型除法器时为 8,但在 Haswell 上为 16-28(取决于数据,除非您的除数是整数,否则更有可能在 28 个周期结束时结束)。这些现代 CPU 具有非常强大的除法器,但其每时钟 2 倍的乘法吞吐量将其击倒。 (当您的代码可以使用 256b AVX 向量自动向量化时更是如此)。另请注意,使用正确的编译器选项,
这些乘法吞吐量也适用于 FMA。
来自 Intel Haswell/Skylake 和 AMD Ryzen 指令表的 http://agner.org/optimize/ 指令表中的数字,用于 SSE 标量(不包括 x87 fmul / fdiv)和 float 或 double 的 256b AVX SIMD 向量。另请参阅x86 标签 wiki。