【问题标题】:Floating point division vs floating point multiplication浮点除法与浮点乘法
【发布时间】:2011-05-06 17:14:36
【问题描述】:

编码是否有任何(非微优化)性能提升

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 引自 cmets:

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

【问题讨论】:

  • 找到答案的真正方法是尝试两者并测量时间。
  • 大多数编译器都会优化像这样的常量表达式,所以没有区别。
  • @sharptooth:是的,尝试自己会解决我的开发机器的问题,但我想如果 SO-crowd 中的某个人已经有了一般情况的答案,他想分享;)
  • @Gabe,我认为 Paul 的意思是将200f / 2 变成100f
  • @Paul:这种优化对于 2 的幂是可能的,但通常不是。除了 2 的幂,没有任何浮点数有一个倒数,你可以乘以它来代替除法。

标签: c++ floating-point micro-optimization


【解决方案1】:

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

如果您查看this answer,您会发现除法可以超过 24 个周期。

为什么除法比乘法花这么多时间?如果您记得回到小学,您可能还记得乘法基本上可以通过许多同时加法来执行。除法需要迭代减法,不能同时执行,因此需要更长的时间。事实上,一些 FP 单元通过执行倒数近似并乘以它来加速除法。它不太准确,但速度更快。

【讨论】:

  • 我认为 OP 想知道导致除法在硬件上比乘法复杂得多的算法/架构要求是什么。
  • 我记得 Cray-1 并没有使用除法指令,它有一个倒数指令,并希望你在那之后进行乘法运算。正是因为这个原因。
  • Mark:确实,CRAY-1 硬件参考的第 3-28 页描述了 4 步除法算法:倒数逼近、倒数迭代、分子 * 逼近、半精度商 * 校正因子.
  • @aaronman:如果 FP 数字存储为 x ^ y,则乘以 x ^ -y 将与除法相同。但是,FP 编号存储为x * 2^y。乘以x * 2^-y 就是乘法。
  • 什么是“小学”?
【解决方案2】:

是的。我所知道的每个 FPU 执行乘法都比除法快得多。

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

所以通常您应该担心使您的代码可读,并让编译器担心使其快速。只有当您对该行有测量速度问题时,您才应该担心为了速度而扭曲您的代码。编译器很清楚什么比他们的 CPU 上的要快,并且通常比您希望的要好得多。

【讨论】:

  • 使代码可读是不够的。有时需要优化某些东西,这通常会使代码难以理解。好的开发人员会首先编写好的单元测试,然后优化代码。可读性很好,但并不总是可以达到的目标。
  • @VJo - 要么你错过了我的倒数第二句话,要么你不同意我的优先事项。如果是后者,恐怕我们注定不同意。
  • 编译器无法为您优化。不允许这样做,因为结果会不同且不符合标准(wrt IEEE-754)。 gcc 为此目的提供了一个-ffast-math 选项,但是它破坏了很多东西并且不能在一般情况下使用。
  • 我想有点死神评论,但除法通常不是流水线式的。因此,它确实可以对性能产生巨大影响。如果有的话,流水线会使乘法和除法的性能差异更大,因为其中一个是流水线的,而另一个不是。
  • C 编译器允许对此进行优化,因为在使用二进制算术时除以 2.0 和乘以 0.5 都是精确的,因此结果是相同的。请参阅 ISO C99 标准的 F.8.2 节,该节将这种情况准确地显示为使用 IEEE-754 绑定时允许的转换。
【解决方案3】:

除法本质上是比乘法慢得多的运算。

这实际上可能是编译器在许多情况下无法(您可能不想)优化的东西,因为浮点数不准确。这两条语句:

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

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

【讨论】:

  • 使用 g++,200.f / 10 和 200.f * 0.1 发出完全相同的代码。
  • @kotlinski:这让 g++ 错了,而不是我的说法。我想有人可能会争辩说,如果差异很重要,你一开始就不应该使用浮点数,但如果我是编译器作者,我肯定只会在更高的优化级别上这样做。
  • @Michael:哪个标准错了?
  • 如果您尝试以公平的方式(不允许编译器优化或替换),您会发现使用双精度的 7 / 10 和 7 * 0.1 不会给出相同的结果。乘法给出了错误的答案,它给出了一个大于除法的数字。浮点数是关于精度的,即使一个位是错误的也是错误的。 7 / 5 != 7/0.2 也是如此,但取一个可以表示 7 / 4 和 7 * 0.25 的数字,将得到相同的结果。 IEEE 支持多种舍入模式,因此您可以克服其中一些问题(如果您提前知道答案)。
  • 顺便说一下,在这种情况下,乘法和除法同样快——它们是在编译时计算的。
【解决方案4】:

答案取决于您正在编程的平台。

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

【讨论】:

  • 但其他答案也很好。除法通常比乘法慢或相等,但这取决于平台。
  • divps 是原始 SSE1 的一部分,在 PentiumIII 中引入。没有 SIMD integer 除法指令,但 SIMD FP 除法确实存在。对于宽向量(尤其是 256b AVX),除法单元的吞吐量/延迟有时比标量或 128b 向量更差。即使是英特尔 Skylake(FP 划分比 Haswell/Broadwell 快得多)也有divps xmm(4 个打包浮点数):11c 延迟,每 3c 吞吐量一个。 divps ymm(8 个打包浮点数):11c 延迟,每 5c 吞吐量一个。 (或打包双打:每 4c 一个或每 8c 一个)有关性能链接,请参阅x86 标签 wiki。
【解决方案5】:

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

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

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

【讨论】:

  • 最近的 Intel CPU(自 Broadwell 以来)use a radix-1024 divider 以更少的步骤完成除法。与几乎所有其他东西不同,除法单元没有完全流水线化(因为正如您所说,缺乏独立性/并行性在硬件中是一个大问题)。例如Skylake 打包双精度除法 (vdivpd ymm) 的吞吐量是乘法 (vmulpd ymm) 的 16 倍,并且在除法硬件功能较弱的早期 CPU 中更差。 agner.org/optimize
【解决方案6】:

Newton rhapson 通过线性代数逼近解决 O(M(n)) 复杂度的整数除法。比 O(n*n) 复杂度更快。

在代码中该方法包含 10mults 9adds 2bitwiseshifts。

这解释了为什么一个除法的 CPU 时间大约是乘法的 12 倍。

【讨论】:

    【解决方案7】:

    对除法要非常小心,并尽可能避免它。例如,将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 / 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.00.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 resultvdivps 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)和 floatdouble 的 256b AVX SIMD 向量。另请参阅 标签 wiki。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-02-14
      • 1970-01-01
      • 2011-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-30
      相关资源
      最近更新 更多