浮点除法与浮点乘法

sum*_*ame 67 c++ floating-point micro-optimization

通过编码是否有任何(非微优化)性能增益

float f1 = 200f / 2
Run Code Online (Sandbox Code Playgroud)

在比较中

float f2 = 200f * 0.5
Run Code Online (Sandbox Code Playgroud)

几年前我的一位教授告诉我,浮点除法比浮点乘法慢,但没有详细说明原因.

这句话适用于现代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
}
Run Code Online (Sandbox Code Playgroud)

更新2 从评论中引用:

[我想]知道什么是算法/架构要求导致>除法在硬件上比复制要复杂得多

Gab*_*abe 74

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

如果你看一下这个答案,你会看到这个分区可以超过24个周期.

为什么除法比乘法要长得多?如果你还记得回到小学,你可能会记得,乘法基本上可以通过多次同时添加来执行.除法需要不能同时执行的迭代减法,因此需要更长的时间.实际上,一些FP单元通过执行倒数近似并乘以它来加速除法.它不是那么准确但有点快.

  • 什么是“小学”? (5认同)
  • 我认为OP想知道什么算法/架构要求导致硬件中的除法比乘法复杂得多。 (2认同)
  • 我记得Cray-1没有用分割指令打扰,它有一个互惠指令,并希望你在那之后成倍增加.正是出于这个原因. (2认同)
  • @aaronman:如果FP数字存储为"x ^ y",则乘以"x ^ -y"将与除法相同.但是,FP编号存储为"x*2 ^ y".乘以`x*2 ^ -y`只是乘法. (2认同)

Mic*_*rdt 19

除了乘法之外,除法本质上是一个慢得多的运算.

事实上,由于浮点不准确,编译器在许多情况下无法(并且您可能不希望)进行优化.这两个陈述:

double d1 = 7 / 10.;
double d2 = 7 * 0.1;
Run Code Online (Sandbox Code Playgroud)

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

  • @kotlinski:这使得g ++错了,而不是我的陈述.我想有人可能会争辩说,如果差异很重要,你不应该首先使用浮点数,但如果我是编译器作者,那肯定是我只会在更高的优化级别上做的事情. (10认同)
  • 如果你以公平的方式(不允许编译器进行优化或替换)尝试它,你会发现使用双精度的7/10和7*0.1不会给出相同的结果.乘法给出了错误的答案,它给出的数字大于除数.浮点是关于精度的,如果即使一个比特关闭也是错误的.同样适用于7/5!= 7/0.2,但是取一个数字,你可以代表7/4和7*0.25,这将得到相同的结果.IEEE支持多种舍入模式,因此您可以克服其中一些问题(如果您提前知道答案). (9认同)
  • 使用g ++,200.f/10和200.f*0.1发出完全相同的代码. (3认同)
  • 顺便提一下,在这种情况下,乘法和除法同样快 - 它们是在编译时计算的. (3认同)
  • @Michael:哪个标准错了? (2认同)
  • -1,错误的逻辑。这里的基本假设是,由于 0.1f 无法准确表示,编译器或芯片将选择一个截然不同的数字。当然不会,它会选择一个非常非常接近 0.1f 的数字——事实上,它可能会选择最好的近似值。因此,`200f*0.1` 的结果将非常接近 `20f`。而且这个数字需要四舍五入,因为无法准确表示“200f*0.1”。现在,考虑到它接近 `20f`,猜猜它会四舍五入到什么? (2认同)

Pet*_*des 16

分裂要非常小心,尽可能避免使用.例如,float inverse = 1.0f / divisor;从循环中提升并inverse在循环内部相乘.(如果inverse可以接受舍入误差)

通常1.0/x不会完全可以表示为floatdouble.当x功率为2 时,它将是精确的.这使编译器可以优化x / 2.0f到结果x * 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),但是他们不能这样做/.


在现代x86 CPU上,除法的延迟比乘法或加法(或FMA)差2到4倍,吞吐量更差6到40 1(对于仅进行除法而不是乘法的紧密循环).

除了@ NathanWhitehead的回答中解释的原因,除法/ sqrt单元没有完全流水线化.最差的比率是256b向量,因为(与其他执行单元不同)除法单元通常不是全宽度,因此宽向量必须分成两半.未完全流水线化的执行单元非常不寻常,以至于英特尔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
Run Code Online (Sandbox Code Playgroud)

你在循环中所做的只是加载/分割/存储,它们是独立的,因此它的吞吐量很重要,而不是延迟.

减少就像accumulator /= b[i]分裂或乘以延迟的瓶颈,而不是吞吐量.但是,如果您在末尾划分或乘以多个累加器,则可以隐藏延迟并仍然使吞吐量饱和.注意延迟或吞吐量的sum += a[i] / b[i]瓶颈,但不是延迟,因为划分不在关键路径上(循环携带的依赖链).adddivdiv


但是在这样的事情中(近似一个函数,比如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;
}
Run Code Online (Sandbox Code Playgroud)

对于log()超过尾数的范围,N阶的两个多项式的比率比具有2N个系数的单个多项式具有更小的误差,并且并行地评估2在单个循环体内提供一些指令级并行性而不是一个大的长度dep chain,使得乱序执行变得更容易.

在这种情况下,我们不会对分频延迟产生瓶颈,因为乱序执行可以在循环中保持循环的多次迭代.

只要我们的多项式足够大,每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,并且具有与实际除法指令一样高的延迟.如果所有你正在做的是分裂,那么它可以是一个吞吐量胜利.(但是如果可以的话,你应该首先避免使用这种循环,可能是通过将除法作为另一个循环的一部分来完成其他工作.)

但是如果你使用除法作为更复杂函数的一部分,那么除了在吞吐量非常低的CPU之外,rcpps自身+额外的mul + FMA通常会更快地除以divps指令divps.

(例如Knight's Landing,见下文.KLL支持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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

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又名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.

来自英特尔Haswell/Skylake和AMD Ryzen的http://agner.org/optimize/指令表中的数字,用于SSE标量(不包括x87 fmul/ fdiv)和256b AVX SIMD向量的floatdouble.另请参阅标记wiki.


T.E*_*.D. 9

是.我所知道的每个FPU都比分割快得多.

然而,现代PC 非常快.它们还包含流水线架构,可以在许多情况下使差异变得可以忽略不计.最重要的是,任何体面的编译器都会执行您在编译时显示的除法操作,并启用优化.对于您更新的示例,任何体面的编译器都会执行该转换.

因此,通常您应该担心使代码可读,并让编译器担心使其快速.只有当您测量到该行的速度问题时,您才会担心为了速度而变换代码.编译器非常清楚什么比它们的CPU更快,并且通常比你希望的好得多.

  • 编译器**不能**为您优化这一点.不允许使用它们,因为结果会有所不同且不符合(IEEE-754).为此目的,gcc提供了一个`-ffast-math`选项,但是它打破了很多东西而且一般不能使用. (13认同)
  • C编译器_are allowed_优化这一点,因为在使用二进制算术时,除以2.0和乘以0.5都是精确的,因此结果是相同的.请参阅ISO C99标准的F.8.2部分,该标准将此情况准确地显示为使用IEEE-754绑定时允许的转换. (11认同)
  • 使代码可读是不够的.有时需要优化某些东西,这通常会使代码难以理解.优秀的开发人员首先会编写好的单元测试,然后优化代码.可读性很好,但并不总是可达目标. (4认同)
  • 我认为这是一个坏消息,但是划分通常不是流水线的.因此,它确实可以对性能产生巨大影响.如果有的话,流水线操作会使乘法和除法的性能差异更大,因为其中一个是流水线的,而另一个则不是. (2认同)

Nat*_*ead 8

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

对于除法,从x的2n位和n位的y开始,你想要计算x/y.最简单的方法是长除法,但是二进制.在每个阶段,你进行比较和减法以获得一个商的一个位.这需要你的步骤.

一些差异:乘法的每一步只需要看1位; 除法的每个阶段都需要在比较期间查看n位.乘法的每个阶段都独立于所有其他阶段(与您添加部分产品的顺序无关); 对于除法,每一步取决于前一步骤.这在硬件方面是一个大问题.如果事情可以独立完成,那么它们可以在一个时钟周期内同时发生.