【问题标题】:How is fma() implementedfma() 是如何实现的
【发布时间】:2015-04-22 05:50:14
【问题描述】:

根据documentationmath.h中有一个fma()函数。这非常好,而且我知道 FMA 的工作原理以及它的用途。但是,我不太确定这在实践中是如何实现的?我最感兴趣的是x86x86_64 架构。

是否有用于 FMA 的浮点(非向量)指令,可能由 IEEE-754 2008 定义?

是使用FMA3还是FMA4指令?

在依赖精度的情况下,是否存在确保使用真正的 FMA 的内在因素?

【问题讨论】:

  • 在 x86 和 x86_64 上,如果被告知允许 gcc 发出 fma 指令(-mfma-mfma4-march=something 其中something 是支持 fma 的处理器)。在 Linux 上,您可以查看 glibc 中的 sysdeps/ieee754/dbl-64/s_fma.c 以了解库函数回退的样子。

标签: floating-point ieee-754 instruction-set fma


【解决方案1】:

实际实现因平台而异,但范围很广:

  • 如果您告诉编译器以具有硬件 FMA 指令(PowerPC、带有 VFPv4 或 AArch64 的 ARM、Intel Haswell 或 AMD Bulldozer 及更高版本)的机器为目标,编译器可能替换调用fma( ) 只需将适当的指令放入您的代码中即可。这不能保证,但通常是很好的做法。否则你会接到数学库的电话,并且:

  • 在具有硬件 FMA 的处理器上运行时,应使用这些指令来实现该功能。但是,如果您的操作系统版本较旧,或者数学库版本较旧,则可能无法利用这些说明。

  • 如果您在没有硬件 FMA 的处理器上运行,或者您使用的是较旧(或不是很好)的数学库,则将使用 FMA 的软件实现。这可以使用巧妙的扩展精度浮点技巧或整数运算来实现。

  • fma( ) 函数的结果应始终正确舍入(即“真正的 fma”)。如果不是,那是系统数学库中的错误。不幸的是,fma( ) 是更难正确实现的数学库函数之一,因此许多实现都有错误。请将它们报告给您的图书馆供应商,以便他们得到修复!

在依赖精度的情况下,是否存在确保使用真正的 FMA 的内在因素?

考虑到一个好的编译器,这不应该是必要的;使用fma( ) 函数并告诉编译器您的目标是什么架构就足够了。但是,编译器并不完美,因此您可能需要在 x86 上使用 _mm_fmadd_sd( ) 和相关的内在函数(但请向您的编译器供应商报告错误!)

【讨论】:

  • “解释圆到奇数的机会就像环法自行车赛:等待很长时间,然后很快就过去了。”
  • @PascalCuoq IEEE-754 默认使用round to even,如果我没记错的话。在这种情况下,为什么圆到奇数相关?我目前正在实现一个多精度库,所以我对内部工作原理有点熟悉,但我还没有听说圆到奇数特别重要。非常有诗意的顺便说一句,干得好!
  • @theswine 如果您的格式是目标 FMA 宽度的两倍,则可以毫无错误地进行乘法运算。假设您正在使用双精度 double 实现 fmaf。剩下的问题是添加(double)a*(double)bdouble 值和float c,并将此添加四舍五入到最接近的float。此操作通常不可用,但可以实现为 double 在奇数舍入中加法,然后从 double 舍入到 float 在最近舍入中。不使用round-to-odd 作为中间结果会导致双舍入问题。
  • 我没有编写我链接到的补丁,但我确实使用幼稚的方法编写了正确的 (AFAICT) fmaf:(假设 a、b、c ≥ 0)ideone.com/kx7MXE。如果你对这个主题感兴趣,你也应该看看这个实现:opensource.apple.com/source/Libm/Libm-315/Source/Intel/…
【解决方案2】:

在软件中实现 FMA 的一种方法是将有效位分为高位和低位。我用Dekker's algorithm

typedef struct { float hi; float lo; } doublefloat;  
doublefloat split(float a) {
    float t = ((1<<12)+1)*a;
    float hi = t - (t - a);
    float lo = a - hi;
    return (doublefloat){hi, lo};
}

拆分浮点数后,您可以像这样使用单个舍入计算a*b-c

float fmsub(float a, float b, float c) {
    doublefloat as = split(a), bs = split(b);
    return ((as.hi*bs.hi - c) + as.hi*bs.lo + as.lo*bs.hi) + as.lo*bs.lo;
}

这基本上是从(ahi,alo)*(bhi,blo) = (ahi*bhi + ahi*blo + alo*bhi + alo*blo) 中减去c

我从论文Extended-Precision Floating-Point Numbers for GPU Computation 中的twoProd 函数和Agner Fog's vector class library 中的mul_sub_x 函数得到这个想法。他使用不同的函数来分割不同分割的浮点向量。我试图在这里重现一个标量版本

typedef union {float f; int i;} u;
doublefloat split2(float a) {
    u lo, hi = {a};
    hi.i &= -(1<<12);
    lo.f = a - hi.f;
    return (doublefloat){hi.f,lo.f};
}

在任何情况下,在fmsub 中使用splitsplit2 与glibc 中数学库中的fma(a,b,-c) 非常一致。无论出于何种原因,我的版本都比 fma 快得多,除非在具有硬件 fma 的机器上(在这种情况下,我还是使用 _mm_fmsub_ss)。

【讨论】:

  • 不错的参考资料。我知道 Schewchuk 和 Priest 的工作。在这个问题中,我对当前指令集中有哪些指令更感兴趣。我猜_mm_fmadd_ss 总结得差不多了。
  • 您的版本可能更快,因为它不处理特殊数字(尤其是无穷大)。我可能错了,但似乎无穷大的乘法/加法会导致 Dekker 算法生成 NaN。我希望运行时在那里正常运行,因此会降低速度。
  • x86 集的内容远不止_mm_fmadd_ss(而且_mm_fmadd_ps 对我来说更有趣)如果你想看到所有这些都去IntrinsicsGuide 并在技术下选择FMA .
  • 计算a*b+c怎么样?
  • @plasmacel,我想你把(as.hi*bs.hi - c)改成(as.hi*bs.hi + c)
【解决方案3】:

不幸的是,Z boson 基于 Dekker 算法的 FMA 建议是不正确的。与 Dekker 的 twoProduct 不同,在更一般的 FMA 情况下,c 的大小相对于乘积项是未知的,因此可能会发生错误的取消。

因此,虽然 Dekker 的 twoProduct 可以通过硬件 FMA 大大加速,但 Dekker 的 twoProduct 的误差项计算不是稳健的 FMA 实现。

正确的实现需要使用高于双精度的求和算法,或者按数量级降序添加项。

【讨论】:

  • 注意他在做fmsub。假设数量是正数,我会说他的实施有效。无论如何,来自 11 xp 的人的精彩评论,干得好。
  • 是的,不,你是对的。如果c 非常小,那么当从ahi*bhi 中减去时,它就会被四舍五入淹没,而且根本没有帮助。他需要形成一个更长的扩展,并从最小的元素开始添加,基本上使用所谓的 Kahan 求和。即使结果被四舍五入为浮点数,这种排序仍然很重要,因为它会影响四舍五入的方向。
  • 我在此处写了一篇关于 Kahan 求和不够充分的简短评论,然后意识到您的真正意思是both,按幅度对输入进行排序,然后使用 Kahan 求和相加.我完全同意这种组合会产生正确舍入的 FMA 结果。
猜你喜欢
  • 2022-01-17
  • 2011-04-26
  • 2015-05-22
  • 2020-07-31
  • 2011-04-24
  • 2016-04-17
  • 2013-08-23
  • 2014-05-27
  • 2016-04-16
相关资源
最近更新 更多