浮点除法与浮点乘法


76

通过编码是否可以获得(非微优化)性能提升

float f1 = 200f / 2

比较

float f2 = 200f * 0.5

我的一位教授几年前告诉我,浮点除法比浮点乘法要慢,但没有详细说明为什么。

此声明适用于现代PC体系结构吗?

更新1

关于评论,请同时考虑以下情况:

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 引用评论:

[我想]知道什么导致/除法在硬件上比乘法复杂得多的算法/体系结构要求


3
找到答案的真正方法是同时尝试并测量时间。
sharptooth 2010年

15
大多数编译器会优化这样的文字常量表达式,因此它没有区别。
Paul R

2
@sharptooth:是的,尝试一下自己可以解决我的开发机的问题,但是我认为,如果SO人群中的某个人已经有了一般情况的答案,他想分享一下;)
sum1stolemyname 2010年

7
@Gabe,我认为Paul的意思是它将200f / 2变成100f
mikerobi,2010年

10
@Paul:这种优化对于2的幂是可能的,但一般而言是不可能的。除了2的幂外,没有浮点数可以倒乘除分母。
R .. GitHub停止帮助ICE,2010年

Answers:


84

是的,许多CPU可以在1个或2个时钟周期内执行乘法运算,但是除法运算总会花费更长的时间(尽管FP除法运算有时比整数除法更快)。

如果您查看此答案,将会看到除法可以超过24个周期。

为什么除法要比乘法花费更多的时间?如果您还记得上小学的话,您可能还记得实际上可以通过许多同时加法来执行乘法。除法需要不能同时执行的迭代减法,因此需要更长的时间。实际上,某些FP单元通过执行倒数逼近并乘以该值来加快除法速度。它不太准确,但是速度更快。


1
我认为OP希望知道什么算法/架构要求会导致除硬件外的乘法要比乘法复杂得多。
chrisaycock 2010年

2
我记得Cray-1并不讨厌除法指令,它有一个倒数指令,并希望您在此之后再乘。正是由于这个原因。
Mark Ransom

1
马克:确实,CRAY-1硬件参考的第3-28页描述了4步除法算法:倒数逼近,倒数迭代,分子*逼近,半精度商*校正因子。
加布

2
@aaronman:如果FP编号存储为x ^ y,则乘以x ^ -y与除法相同。但是,FP编号存储为x * 2^y。乘以x * 2^-y仅仅是乘法。
加布

4
什么是“小学”?
法拉普

31

请小心分割,并尽可能避免分割。例如,float inverse = 1.0f / divisor;从循环中提升并在循环中乘以inverse。(如果舍入误差inverse可以接受)

通常1.0/x不会完全表示为floatdouble。精确到x是2的幂次方。这使编译器可以进行优化x / 2.0fx * 0.5f不会改变结果。

为了使编译器即使结果不精确(或使用运行时变量除数)也可以为您进行优化,您需要使用诸如之类的选项 gcc -O3 -ffast-math。具体来说,-freciprocal-math(在启用时-funsafe-math-optimizations启用-ffast-math)可以使编译器在有用时替换x / yx * (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;  // 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延迟,因为除法不在关键路径(循环携带的依赖链)上。


但是在这样的事情中(以两个多项式之比近似一个函数log(x)),除法可以很便宜

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()用例中,有大量工作要提取指数/尾数,然后再将它们重新组合在一起,因此在两次划分之间还有更多工作要做。)


当您确实需要分割时,通常最好只分割而不是 rcpps

x86有一个近似倒数指令(rcpps),只能给您12位精度。(AVX512F具有14位,AVX512ER具有28位。)

您可以执行此操作x / y = x * approx_recip(y)而无需使用实际的除法指令。(rcppsitsef相当快;通常比乘法慢一些。它使用从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矢量floatdouble。另请参阅 标签Wiki。


20

从本质上来说,除法运算比乘法慢得多。

实际上,由于浮点数的不正确,在很多情况下,编译器可能无法(而且您可能不希望)进行优化。这两个语句:

double d1 = 7 / 10.;
double d2 = 7 * 0.1;

在语义上完全相同-0.1不能精确地表示为a double,因此最终会使用稍有不同的值-在这种情况下,将乘法替换为除法将产生不同的结果!


3
对于g ++,200.f / 10和200.f * 0.1会发出完全相同的代码。
约翰·科特林斯基

10
@kotlinski:这会使g ++错误,而不是我的陈述。我想有人可能会争辩说,如果差异很重要,那么您不应该首先使用浮点数,但是绝对可以肯定的是,如果我是编译器作者,那么我只会在更高的优化级别上这样做。
Michael Borgwardt 2010年

3
@Michael:哪个标准错误?
约翰·科特林斯基

9
如果您尝试使用它,以一种公平的方式(不允许编译器进行优化或替代),您会发现使用双精度的7/10和7 * 0.1不会给出相同的结果。乘法给出错误的答案,给出的数字大于除数。浮点数与精度有关,即使只有一点点都错了,这也是错误的。7/5!= 7 / 0.2也是一样,但是取一个可以代表7/4和7 * 0.25的数字,将得到相同的结果。IEEE支持多种舍入模式,因此您可以克服其中的一些问题(如果您提前知道答案)。
old_timer 2010年

6
顺便说一句,在这种情况下,乘法和除法运算同样快-它们是在编译时计算的。
约翰·科特林斯基

10

是。我知道的每个FPU的乘法运算都快于除法运算。

但是,现代PC速度非常快。它们还包含流水线架构,这些架构在许多情况下都可以忽略不计。最重要的是,任何体面的编译器都会在打开优化的情况下执行您在编译时显示的除法运算。对于您的更新示例,任何体面的编译器都会自行执行该转换。

因此,通常您应该担心使代码具有可读性,而让编译器担心使其变得快速。仅当该行的速度有实际问题时,才需要担心为了速度而使代码变形。编译器清楚地知道什么比其CPU上的速度更快,并且通常是比您期望的更好的优化器。


4
使代码可读性还不够。有时需要优化某些内容,这通常会使代码难以理解。好的开发人员会首先编写好的单元测试,然后再优化代码。可读性很好,但并非总是可以实现的目标。
BЈовић

@VJo-您错过了我的倒数第二句话,或者您不同意我的优先顺序。如果是后者,恐怕我们注定会不同意。
TED

14
编译器无法为您优化此设置。不允许这样做,因为结果会有所不同且不一致(wrt IEEE-754)。gcc-ffast-math为此提供了一个选项,但是它破坏了很多东西,因此通常无法使用。
R .. GitHub停止帮助ICE,2010年

2
我猜想有点不可思议,但通常不进行分部处理。因此,它确实可以大大降低性能。如果有的话,流水线化会使得乘法和除法性能的差异更大,因为其中一个流水线却没有。
哈罗德

11
允许C编译器对此进行优化,因为使用二进制算术时,除以2.0和乘以0.5都是精确的,因此结果是相同的。请参阅ISO C99标准的F.8.2节,该节将使用IEEE-754绑定时的情况正确显示为允许的转换。
njuffa 2012年

8

考虑两个n位数字相乘需要什么。使用最简单的方法,您可以取一个数字x并反复移位,然后有条件地将其添加到累加器中(基于另一个数字y中的一位)。添加n次后,您就完成了。您的结果适合2n位。

对于除法,您要从2n位的x和n位的y开始,要计算x / y。最简单的方法是长除法,但采用二进制。在每个阶段,您都需要进行比较和减法以获得更多的商。这需要您n步。

有一些区别:乘法的每一步只需要看1位。在比较期间,除法的每个阶段都需要查看n位。乘法的每个阶段都独立于所有其他阶段(与添加部分乘积的顺序无关);对于划分,每个步骤取决于上一步。这在硬件方面意义重大。如果事情可以独立完成,那么它们可以在一个时钟周期内同时发生。


最近的Intel CPU(自Broadwell起)使用radix-1024除法器以较少的步骤完成除法。与几乎所有其他东西不同,除法单元没有完全流水线化(因为正如您所说,缺乏独立性/并行性在硬件方面很重要)。例如,Skylake压缩双精度除法(vdivpd ymm)的吞吐量比乘法(vmulpd ymm)差16倍,而在功能不那么强大的除法硬件的早期CPU中,这种情况更糟。 agner.org/optimize
彼得·科德斯

2

牛顿·拉普森通过线性代数逼近求解O(M(n))复杂度的整数除法。比原本的O(n * n)复杂度更快。

在代码中,该方法包含10个乘法9加2位向移位。

这就解释了为什么除法的CPU滴答数大约是乘法的12倍。


1

答案取决于您要为其编程的平台。

例如,在x86上的数组上执行大量乘法运算要比进行除法运算快得多,因为编译器应创建使用SIMD指令的汇编代码。由于SIMD指令中没有除法运算,因此使用乘除法运算会带来很大的改进。


但是其他答案也很好。除法运算通常比乘法运算慢或相等,但取决于平台。
2010年

1
到目前为止,有上交所的划分指令
Andre Holzner

divps是PentiumIII中引入的原始SSE1的一部分。没有SIMD整数除法指令,但确实存在SIMD FP除法。对于宽向量(尤其是256b AVX),除法单元的吞吐率/等待时间甚至比标量或128b向量还要差。甚至Intel Skylake(FP划分比Haswell / Broadwell都要快得多)也有divps xmm(4个压缩浮点数):11c延迟,每3c吞吐量一个。 divps ymm(8个压缩浮点数):11c延迟,每5c吞吐量一个。(或者对于打包双打:每4c或8c之一)有关性能链接,请参见x86标签Wiki。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.