请小心分割,并尽可能避免分割。例如,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循环的自动矢量化(尤其是减少)(例如,将数组求和为一个标量)通常常常很重要。 GCC为什么不将a * a * a * a * a * a优化为(a * a * a)*(a * a * a)?
还要注意的是C ++编译器可以折叠+
并*
为FMA在某些情况下(编译为目标,支持它,想什么时候-march=haswell
),但他们不能做到这一点与/
。
司具有比乘法或加法(或更坏的延迟FMA)通过因子2到4对现代的x86 CPU,更糟的吞吐量的6〜40的因子1(为一个紧密循环做仅分裂,而不是仅倍增)。
由于@NathanWhitehead的答案中所述的原因,divide / sqrt单元未完全流水线化。最差的比率是针对256b向量,因为(与其他执行单元不同)除法单元通常不是全角的,因此必须将向量分成两半。没有完全流水线化的执行单元非常罕见,以至于Intel CPU都有一个arith.divider_active
硬件性能计数器,可帮助您找到导致分频器吞吐量瓶颈的代码,而不是通常的前端或执行端口瓶颈。(或更常见的情况是,内存瓶颈或较长的延迟链限制了指令级并行性,导致指令吞吐量每时钟小于〜4)。
但是,在Intel和AMD CPU(KNL除外)上的FP划分和sqrt是作为单个uop实现的,因此它不一定会对周围的代码产生很大的吞吐量影响。最好的除法情况是,无序执行可以隐藏等待时间,并且在除法的同时发生大量的乘法和加法(或其他工作)。
(整数除法的微码作为英特尔多个微操作,所以它总是对周围的代码更具冲击力的是整数乘法有高性能的整数除法需求较少,所以对硬件的支持是不是幻想相关:。像微码指令idiv
即可导致对齐敏感的前端瓶颈。)
因此,例如,这将非常糟糕:
for ()
a[i] = b[i] / scale;
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv;
您在循环中要做的只是加载/划分/存储,它们是独立的,因此吞吐量很重要,而不是延迟。
像减少这样的accumulator /= b[i]
瓶颈会限制分频或倍增延迟,而不是吞吐量。但是,如果使用多个累加器最后进行除法或乘法运算,则可以隐藏等待时间,但仍会使吞吐量饱和。请注意,sum += a[i] / b[i]
瓶颈在于add
延迟或div
吞吐量,但不是div
延迟,因为除法不在关键路径(循环携带的依赖链)上。
但是在这样的事情中(以两个多项式之比近似一个函数log(x)
),除法可以很便宜:
for () {
float p = polynomial(b[i], 1.23, -4.56, ...);
float q = polynomial(b[i], 3.21, -6.54, ...);
a[i] = p/q;
}
对于log()
尾数的范围,两个N阶多项式的比率要比具有2N个系数的单个多项式的错误要小得多,并且并行求2会在单个循环体内提供一些指令级的并行性,而不是一个冗长的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个乘法和2个FMA,并且延迟大约与实际的除法指令一样高。如果所有你正在做的是分裂,那么它可以是一个吞吐量胜利。(但是,如果可以的话,您应该首先避免这种循环,可能是将除法运算作为另一个执行其他工作的循环的一部分。)
但是,如果将除法用作更复杂功能的一部分,则rcpps
其本身+额外的mul + FMA通常可以使仅通过一条divps
指令进行除法就更快,除非在divps
吞吐量非常低的CPU上。
(例如,Knight's Landing,请参见下文。KNL支持AVX512ER,因此对于float
矢量而言,其VRCP28PS
结果已经足够准确,即使不进行Newton-Raphson迭代也可以相乘。 float
尾数大小仅为24位。)
Agner Fog表中的特定数字:
与其他所有ALU操作不同,除法延迟/吞吐量取决于某些CPU的数据。同样,这是因为它是如此缓慢并且没有完全流水线化。有固定延迟的情况下,无序调度更容易,因为它避免了回写冲突(当同一执行端口试图在同一周期中产生2个结果时,例如,先运行3个周期的指令,然后执行两个1周期的操作) 。
通常,最快的情况是除数是“”2.0
或“ 0.5
(即base2float
表示在尾数中有很多尾随零)”这样的“整数”数。
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的此InstLatx64结果。数字为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。
从数字http://agner.org/optimize/英特尔的Haswell / SKYLAKE微架构和AMD Ryzen,对于标量SSE(不包括的x87指令表fmul
/ fdiv
)和256B的AVX SIMD矢量float
或double
。另请参阅x86 标签Wiki。