我的 fma() 坏了吗?

Posted

技术标签:

【中文标题】我的 fma() 坏了吗?【英文标题】:Is my fma() broken? 【发布时间】:2017-02-10 18:46:29 【问题描述】:

在使用double fma(double x, double y, double z); 时,我希望在下面标有'?' 的输出行中出现非零d。它似乎在内部仅使用long double 精度,而不是指定的无限精度

fma 函数计算 (x × y) + z,作为一个三元运算四舍五入:它们将值(好像)计算为无限精度并舍入一次到结果格式,根据当前的舍入模式。 §7.12.13.1 2(我的重点)

我的fma() 是否损坏了,或者我如何在代码或编译选项中错误地使用它?

#include <float.h>
#include <math.h>
#include <stdio.h>

int main(void) 
  // Invoking: Cygwin C Compiler
  // gcc -std=c11 -O0 -g3 -pedantic -Wall -Wextra -Wconversion -c -fmessage-length=0 
  //   -v -MMD -MP -MF"x.d" -MT"x.o" -o "x.o" "../x.c"

  printf("FLT_EVAL_METHOD %d\n", FLT_EVAL_METHOD);
  for (unsigned i = 20; i < 55; i++) 
    volatile double a = 1.0 + 1.0 / pow(2, i);
    volatile double b = a;
    volatile double c = a * b;
    volatile double d = fma(a, b, -c);
    volatile char *nz = ((i >= 27 && a != 1.0) == !d) ? "?" : "";
    printf("i:%2u a:%21.13a c:%21.13a d:%10a %s\n", i, a, c, d, nz);
  
  return 0;

输出

FLT_EVAL_METHOD 2
i:20 a: 0x1.0000100000000p+0 c: 0x1.0000200001000p+0 d:    0x0p+0 
i:21 a: 0x1.0000080000000p+0 c: 0x1.0000100000400p+0 d:    0x0p+0 
i:22 a: 0x1.0000040000000p+0 c: 0x1.0000080000100p+0 d:    0x0p+0 
i:23 a: 0x1.0000020000000p+0 c: 0x1.0000040000040p+0 d:    0x0p+0 
i:24 a: 0x1.0000010000000p+0 c: 0x1.0000020000010p+0 d:    0x0p+0 
i:25 a: 0x1.0000008000000p+0 c: 0x1.0000010000004p+0 d:    0x0p+0 
i:26 a: 0x1.0000004000000p+0 c: 0x1.0000008000001p+0 d:    0x0p+0 
i:27 a: 0x1.0000002000000p+0 c: 0x1.0000004000000p+0 d:   0x1p-54 
i:28 a: 0x1.0000001000000p+0 c: 0x1.0000002000000p+0 d:   0x1p-56 
i:29 a: 0x1.0000000800000p+0 c: 0x1.0000001000000p+0 d:   0x1p-58 
i:30 a: 0x1.0000000400000p+0 c: 0x1.0000000800000p+0 d:   0x1p-60 
i:31 a: 0x1.0000000200000p+0 c: 0x1.0000000400000p+0 d:   0x1p-62 
i:32 a: 0x1.0000000100000p+0 c: 0x1.0000000200000p+0 d:    0x0p+0 ?
i:33 a: 0x1.0000000080000p+0 c: 0x1.0000000100000p+0 d:    0x0p+0 ?
i:34 a: 0x1.0000000040000p+0 c: 0x1.0000000080000p+0 d:    0x0p+0 ?
...
i:51 a: 0x1.0000000000002p+0 c: 0x1.0000000000004p+0 d:    0x0p+0 ?
i:52 a: 0x1.0000000000001p+0 c: 0x1.0000000000002p+0 d:    0x0p+0 ?
i:53 a: 0x1.0000000000000p+0 c: 0x1.0000000000000p+0 d:    0x0p+0 
i:54 a: 0x1.0000000000000p+0 c: 0x1.0000000000000p+0 d:    0x0p+0 

版本信息

gcc -v

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-pc-cygwin/5.3.0/lto-wrapper.exe
Target: i686-pc-cygwin
Configured with: /cygdrive/i/szsz/tmpp/gcc/gcc-5.3.0-5.i686/src/gcc-5.3.0/configure --srcdir=/cygdrive/i/szsz/tmpp/gcc/gcc-5.3.0-5.i686/src/gcc-5.3.0 --prefix=/usr --exec-prefix=/usr --localstatedir=/var --sysconfdir=/etc --docdir=/usr/share/doc/gcc --htmldir=/usr/share/doc/gcc/html -C --build=i686-pc-cygwin --host=i686-pc-cygwin --target=i686-pc-cygwin --without-libiconv-prefix --without-libintl-prefix --libexecdir=/usr/lib --enable-shared --enable-shared-libgcc --enable-static --enable-version-specific-runtime-libs --enable-bootstrap --enable-__cxa_atexit --with-dwarf2 --with-arch=i686 --with-tune=generic --disable-sjlj-exceptions --enable-languages=ada,c,c++,fortran,java,lto,objc,obj-c++ --enable-graphite --enable-threads=posix --enable-libatomic --enable-libcilkrts --enable-libgomp --enable-libitm --enable-libquadmath --enable-libquadmath-support --enable-libssp --enable-libada --enable-libjava --enable-libgcj-sublibs --disable-java-awt --disable-symvers --with-ecj-jar=/usr/share/java/ecj.jar --with-gnu-ld --with-gnu-as --with-cloog-include=/usr/include/cloog-isl --without-libiconv-prefix --without-libintl-prefix --with-system-zlib --enable-linker-build-id --with-default-libstdcxx-abi=gcc4-compatible
Thread model: posix
gcc version 5.3.0 (GCC) 

【问题讨论】:

如果它有任何意义,这会在我的 mac 上使用 clang 3.8 dist 出现彩虹和蝴蝶。找不到? @WhozCraig 看来我正在经历this related answer 的第 4 点,这与您的好平台不同。 好吧,那糟透了。 (就像我不得不告诉你的那样)。对不起,伙计。 @chux 使用 gcc 6.3.1 一切看起来都很好!没有? 可见。 听起来你的数学库有问题:生成的可执行文件上的ldd 给出了什么? 【参考方案1】:

这是 Cygwin 的错。或者更准确地说,它使用的 newlib C 库。 explicitly says 它甚至没有尝试使 fma() 仿真正确。

自 2015 年以来,GNU C 库对几乎所有 fma 变体都有正确的仿真。有关详细信息以及用于实现此功能的补丁,请参阅源软件错误 13304。

如果效率不是问题,那么我会简单地使用例如

#if defined(__CYGWIN__) && !defined(__FMA__) && !defined(__FMA3__) && !defined(__FMA4__)
#define fma(x, y, z)  fma_emulation(x, y, z)

double fma_emulation(double x, double y, double z)

    /* One of the implementations linked above */

#endif

我个人根本不使用 Windows,但如果有人使用(使用 Windows 并需要 fma 仿真),我建议他们尝试向上游推送补丁,并带有指向 GNU C library discussion on correct fma emulation 的链接。


我想知道的是,是否可以仅检查结果的低 M 位(舍入中丢弃)以确定结果中 ULP 的正确值,以及使用nextafter() 相应地调整使用简单的a×b+c 操作获得的结果;而不是使用多精度算术来实现整个操作。

编辑:不,因为加法可能会溢出,丢弃一个额外的位作为丢弃部分的 MSB。仅出于这个原因,我们确实需要完成整个操作。另一个原因是,如果 a×bc 有不同的符号,那么我们不是加法,而是从较大的量级中减去较小的量级(结果使用较大的符号),这可能会导致清除较大的几个高位,并影响整个结果的哪些位在舍入中被丢弃。

但是,对于 x86 和 x86-64 架构上的 IEEE-754 Binary64 double,我相信使用 64 位(整数)寄存器和 128 位乘积的 fma 仿真仍然非常可行。我将试验一个表示,其中 64 位寄存器中的低 2 位用于舍入决策位(LSB 是所有丢弃位的逻辑或),53 位用于尾数,一个进位位,剩下 8未使用和忽略的高位。当无符号整数尾数转换为(64 位)双精度时执行舍入。如果这些实验产生任何有用的东西,我会在这里描述它们。


初步发现:fma() 在 32 位系统上的仿真速度很慢。 387 FPU 上的 80 位东西在这里基本上没用,在 32 位系统上实现 53×53 位乘法(和位移)只是......不值得努力。在我看来,上面链接的 glibc fma() 仿真代码已经足够好了。

其他发现:处理非有限值是讨厌。 (次正规数只是有点烦人,需要特殊处理(因为尾数中的隐式 MSB 为零)。)如果三个参数中的任何一个是非有限的(无穷大或某种形式的 NaN),则返回 a*b + c(未融合) 是唯一明智的选择。处理这些情况需要额外的分支,这会减慢仿真速度。

最终决定:以优化方式处理的案例数量(而不是使用 glibc 仿真中使用的多精度“肢体”方法)足以使这种方法不值得努力。如果每个肢体是 64 位,则 abc 中的每一个都最多分布在 2 个肢体上,并且 a ×b 超过三肢。 (对于 32 位肢体,分别只有 3 个和 5 个肢体。)取决于 a×bc 是否具有相同的或不同的符号,只有两种根本不同的情况需要处理——在不同符号的情况下,加法变成减法(从大到小,结果与较大的值得到相同的符号)。

简而言之,多精度方法更好。所需的实际精度非常有限,甚至不需要动态分配。如果 ab 的尾数乘积可以高效计算,那么多精度部分仅限于保持乘积和处理加法/减法。最终舍入可以通过将结果转换为 53 位尾数、指数和两个额外的低位来完成(较高的是舍入中丢失的最高有效位,而较低的是舍入中丢失的其余位的 OR四舍五入)。本质上,关键操作可以使用整数(或 SSE/AVX 寄存器)完成,从 55 位尾数到 double 的最终转换根据当前规则处理舍入。

【讨论】:

"可能只检查结果的低 M 位(舍入舍弃)" --> 我不会这么说。丢弃的位包括2M 乘积的较低M 位和cM 位,它们可能远离“右侧”并有助于该回合。正如我所看到的,精确的总和可能具有高达 (2*M + |Ae + Be - 2*Ce|) 的位宽(或者很大程度上取决于 AB 与 C 的指数差。(假设位 [0 ] 是 MSBit) 和的 bit[M-1]、bit[M] 和所有次要位的“或”对这一轮有贡献。 感觉就像史努比对着cygwin fma/Red-Barron挥舞拳头 @chux:不,不要被“无限精度”的类比所迷惑。我们只需要考虑影响舍入的位。有六种基本情况: ab 和 c 具有相同的指数和相同的符号;不同的标志; ab 具有更大的指数,但符号与 c 相同;不同的标志; ab 的指数比 c 小,但符号相同;和不同的标志。对于四种 IEEE-754 舍入模式,我们只需要知道舍入时丢弃的部分的最高有效位,我们可以在使用 2M 位寄存器的六种情况中的每一种情况下做到这一点。我应该详细说明这一点吗? (有用吗?) 对于默认的 IEEE 舍入 ties to even,需要删除部分的 MSBit(我们同意这一点)并且如果任何其他删除的位为 1。(我们出现不同意)。如果任何其他丢弃的位为 1,则该值 高于 中途并从 0 舍入。如果所有其他丢弃的位为 0,则该值四舍五入,因为它是一个 领带。所有丢弃位都会影响该舍入模式。 @chux:所以,我的想法没有成功。但是,您关于舍入位对舍入的影响的注释提醒我,如果我们打包尾数的 53 个最高有效位,丢弃位的最高有效位,以及一个额外的位,即其余位的逻辑或在丢弃的位中,我们得到一个 55 位无符号整数,当转换为双精度时,该整数会正确舍入。这意味着使用少量(固定上限)肢体的多精度方法应该有效地处理仿真。舍入方法可能很新颖,但我对此表示怀疑。

以上是关于我的 fma() 坏了吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何以编程方式检查 CPU 上是不是启用了 fused mul add (FMA) 指令?

FMA 指令显示为三个压缩双操作?

javaws退出代码真的坏了吗?

所有用例的双重检查锁都坏了吗?

mysql_real_escape_string() 坏了吗?

SQLFiddle 坏了吗? Oracle、SQL Server 的错误...? [关闭]