通过编码是否有任何(非微优化)性能增益
float f1 = 200f / 2
与
比较float f2 = 200f * 0.5
我的一位教授几年前告诉我,浮点除法比浮点乘法慢,但没有详细说明原因。
这句话适用于现代PC架构吗?
UPDATE1
关于评论,请同时考虑这个案例:
float f1;
float f2 = 2
float f3 = 3;
for( i =0 ; i < 1e8; i++)
{
f1 = (i * f2 + i / f3) * 0.5; //or divide by 2.0f, respectively
}
更新2 引用评论:
[我想]知道导致&gt;的算法/架构要求是什么?除了乘法之外,硬件要比硬件复杂得多。
答案 0 :(得分:71)
是的,许多CPU可以在1或2个时钟周期内执行乘法,但除法总是需要更长时间(尽管FP除法有时比整数除法更快)。
如果你看this answer,你会看到该分裂可以超过24个周期。
为什么除法比乘法要长得多?如果你还记得回到小学,你可能会记得,乘法基本上可以通过多次同时添加来执行。除法需要不能同时执行的迭代减法,因此需要更长的时间。实际上,一些FP单元通过执行倒数近似并乘以它来加速除法。它不是那么准确,但有点快。
答案 1 :(得分:18)
除了乘法之外,除法本质上要慢得多。
实际上,由于浮点不准确,这可能是编译器无法(并且您可能不希望)在许多情况下进行优化的事情。这两个陈述:
double d1 = 7 / 10.;
double d2 = 7 * 0.1;
不在语义上相同 - 0.1
无法准确表示为double
,因此最终会使用稍微不同的值 - 将乘法替换为除法这种情况会产生不同的结果!
答案 2 :(得分:12)
对分裂要非常小心,并尽可能避免分裂。例如,将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可能会使一些&#34;不安全&#34;默认优化(我认为确实如此,但我忘了)。
-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
),但他们可以&#39 ;用/
来做。
在现代x86 CPU上,分区的延迟比乘法或加法(或FMA)差2到4倍,吞吐量更差6到40 1 (对于仅执行 除法的紧密循环而不是仅乘法)。
除了@NathanWhitehead's answer中解释的原因,除法/ sqrt单位没有完全流水线化。最差的比率是256b向量,因为(与其他执行单元不同)除法单元通常不是全宽度,因此宽向量必须分成两半。未完全流水线化的执行单元非常不寻常,以至于英特尔CPU具有arith.divider_active
硬件性能计数器,可帮助您找到导致分频器吞吐量出现瓶颈的代码,而不是通常的前端或执行端口瓶颈。 (或者更常见的是,内存瓶颈或长延迟链限制了指令级并行性,导致指令吞吐量低于每个时钟约4个。)
但是,Intel和AMD CPU(KNL除外)上的FP划分和sqrt是作为单个uop实现的,因此它不一定会对周围代码产生巨大的吞吐量影响。除法的最佳情况是,无序执行可以隐藏延迟,并且当存在大量的乘法和增加(或其他工作)时,可以与除法并行发生。
(整数除法在英特尔上被编码为多个uop,因此它总是对整数乘法的周围代码产生更大的影响。对高性能整数除法的需求较少,因此硬件支持不是相关:microcoded instructions like idiv
can cause alignment-sensitive front-end bottlenecks。)
例如,这将非常糟糕:
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给出了一些指令级并行性。单循环体而不是一个长度很长的dep链,使得乱序执行变得更容易。
在这种情况下,我们不会对分频延迟产生瓶颈,因为无序执行可以在循环中保持循环的多次迭代。
只要我们的多项式足够大,每10条FMA指令只有一个除法,我们就不会出现除吞吐量的瓶颈。 (在一个真实的log()
用例中,有一堆工作提取指数/尾数并将事物重新组合在一起,所以在除法之间还有更多的工作要做。)< / p>
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
指令,除了在吞吐量divps
非常低的CPU上。
(例如Knight&#39; s登陆,见下文.KLL支持AVX512ER,因此对于float
向量,VRCP28PS
结果已经足够准确,只能在没有牛顿的情况下相乘Raphson迭代。float
尾数大小只有24位。)
与其他每个ALU操作不同,除法延迟/吞吐量取决于某些CPU的数据。同样,这是因为它太慢而且没有完全流水线化。固定延迟会使乱序调度更容易,因为它避免了回写冲突(当同一执行端口尝试在同一周期产生2个结果时,例如运行3周期指令然后执行两个1周期操作)
一般来说,最快的情况是当除数是&#34; round&#34;像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具有低功耗 per-core ,并且在芯片上封装大量内核可以使Skylake-AVX512具有更严格的芯片面积限制。似乎AVX512ER rcp28ps
/ pd
就是您所想要的&#34;在KNL上使用。
(有关Skylake-AVX512又名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。
来自英特尔Haswell / Skylake和AMD Ryzen的http://agner.org/optimize/指令表中的数字,用于SSE标量(不包括x87 fmul
/ fdiv
)和用于{{1的256b AVX SIMD向量}或float
。另请参阅x86标记wiki。
答案 3 :(得分:9)
是。我所知道的每个FPU都比分割快得多。
然而,现代PC 非常快。它们还包含流水线架构,可以在许多情况下使差异变得可以忽略不计。最重要的是,任何体面的编译器都会执行您在编译时中显示的除法操作,并启用优化。对于您更新的示例,任何体面的编译器都会自行执行该转换。
所以一般你应该担心让你的代码可读,让编译器担心让它变快。只有当您测量到该行的速度问题时,您才会担心为了速度而变换代码。编译器非常清楚什么比它们的CPU更快,并且通常比你希望的更好的优化器。
答案 4 :(得分:6)
考虑两个n位数乘法所需的内容。使用最简单的方法,您可以取一个数字x并重复移位并有条件地将其添加到累加器(基于另一个数字y中的位)。添加n后,你就完成了。你的结果适合2n位。
对于除法,从2位的x和n位的y开始,你想要计算x / y。最简单的方法是长除法,但是二进制。在每个阶段,你进行比较和减法以获得一个商的一个位。这需要你步骤。
一些差异:乘法的每一步只需要看1位;除法的每个阶段都需要在比较期间查看n位。乘法的每个阶段都独立于所有其他阶段(与您添加部分产品的顺序无关);对于除法,每一步取决于前一步骤。这在硬件方面是一个大问题。如果事情可以独立完成,那么它们可以在一个时钟周期内同时发生。
答案 5 :(得分:2)
Newton rhapson通过线性代数近似求解O(M(n))复杂度中的整数除法。比其他O(n * n)复杂度更快。
在代码中该方法包含10个结果9adds 2bitwiseshifts。
这解释了为什么一个除法与乘法一样多的cx滴答数的12倍。
答案 6 :(得分:1)
答案取决于您编程的平台。
例如,在x86上对数组进行大量乘法应该比分割快得多,因为编译器应该创建使用SIMD指令的汇编代码。由于SIMD指令中没有除法,因此使用乘法除法可以看到很大的改进。