编译器是否适合实现使用融合乘法/加法编写的点积dot({a,c}, {b,d}) := a*b + c*d
,给出 fl(⋅ + fl(⋅)) 就好像它是编写的一样fma(a,b, c*d)
?一般不会!
以下是根据 W. Kahan 在 IEEE 754 上的讲义改编的几个示例:
假设我们要评估平方差² − ²。
这可以写成点积dot({x,y}, {-x,y}) = x*x - y*y
。这个幼稚的公式在 ≈ 时会遭受灾难性的抵消,但至少在 = 时,它可靠地返回零,因为 fl(fl(²) − fl(²)) = fl(fl(²) − fl(²)) = fl( 0) = 0。
这可以用 FMA 作为 来计算fma(x,x, -y*y)
。但是如果 = = fl(1.234) = 0x1.3be76c8b43958p+0 那么结果是 -1.3532e7b3d8ep-55 ≈ -3.352 × 10⁻¹⁷,在 IEEE 754 binary64 算术中,而不是我们可能希望的零。
它不仅非零,而且是负数,所以如果你尝试在下游取平方根,即使你可以保证上游 ≥ ,你也会遇到 NaN 。
(当然,分解(x + y)*(x - y)
可以更好地避免中间的灾难性取消,但这个问题是关于在没有额外假设的情况下评估点积。)
假设我们要在直角坐标 ( + )⋅( + ) = ( − ) + ( + ) 中评估复数乘积。
它的虚部可以写成点积dot({a,d}, {b,c}) = a*d + b*c
。它可以用 FMA 作为 来计算fma(a,d, b*c)
。
你可能期望复数 + 与其复共轭 - 的乘积是实数,虚部为零——如果用 计算,它确实如此a*d + b*c
,但如果用 计算,则不是fma(a,d, b*c)
。例如,如果 = fl(1.234) = 1.3be76c8b43958p+0 和 = fl(5.678) = 1.6b645a1cac083p+2,则 fl(⋅(−) + fl(⋅)) = −1.6f6512a94ffp−55 ≈ 3.983 × 10⁻ ¹⁷。
因此,在这些场景中使用 FMA 是一种糟糕的编译器形式,而无需通过fma(a,b, c*d)
使用fma
函数 from编写<math.h>
或添加#pragma STDC FP_CONTRACT ON
以授权此类恶作剧来明确要求它。
就是说……说服 GCC 10.2 滥用 vfmadd231sd似乎并不难,只需传递-O2 -march=haswell
,即使是明确的#pragma STDC FP_CONTRACT OFF
,对于 ICC 21.1.9 也是如此。这在我看来就像一个有缺陷的优化器!相比之下,Clang 11.0.1 使用带有 的 vfmadd231sd#pragma STDC FP_CONTRACT ON
,但不使用省略或设置为的编译指示OFF
。