[英]Why does this simple program compiled with gcc,-mfpmath=387, and an optimization level of -O2 or -O3 produce NaN values?
我有一个执行数值计算的短程序,当某些特定条件成立时,会得到不正确的 NaN 结果。 我看不出这个 NaN 结果是如何产生的。 请注意,我没有使用允许重新排序算术运算的编译器选项,例如-ffath-math
。
问题:我正在寻找 NaN 结果如何产生的解释。 在数学上,计算中没有任何东西会导致除以零或类似的东西。 我错过了一些明显的东西吗?
请注意,我不是在问如何解决问题——这很容易。 我只是想了解 NaN 是如何出现的。
请注意,此示例非常脆弱,即使是很小的修改(例如在循环中添加printf()
调用以观察值)也会改变行为。 这就是为什么我无法进一步最小化它的原因。
// prog.c
#include <stdio.h>
#include <math.h>
typedef long long myint;
void fun(const myint n, double *result) {
double z = -1.0;
double phi = 0.0;
for (myint i = 0; i < n; i++) {
double r = sqrt(1 - z*z);
/* avoids division by zero when r == 0 */
if (i != 0 && i != n-1) {
phi += 1.0 / r;
}
double x = r*cos(phi);
double y = r*sin(phi);
result[i + n*0] = x;
result[i + n*1] = y;
result[i + n*2] = z;
z += 2.0 / (n - 1);
}
}
#define N 11
int main(void) {
// perform computation
double res[3*N];
fun(N, res);
// output result
for (int i=0; i < N; i++) {
printf("%g %g %g\n", res[i+N*0], res[i+N*1], res[i+N*2]);
}
return 0;
}
编译:
gcc -O3 -mfpmath=387 prog.c -o prog -lm
output 的最后一行是:
nan nan 1
我希望数字接近于零,而不是 NaN。
以下必须全部成立才能出现 NaN output:
在 x86 平台上用 GCC 编译。 我能够在 macOS 10.14.6 上使用 GCC 12.2.0(来自 MacPorts)进行重现,在 Linux(openSUSE Leap 15.3)上使用 GCC 版本 9.3.0、8.3.0 和 7.5.0 进行重现。
我无法在 Linux 上使用 GCC 10.2.0 或更高版本,或者在 macOS 上使用 GCC 11.3.0 重现它。
选择使用带有-mfpmath=387
的 x87 指令,以及-O2
或-O3
的优化级别。
myint
必须是带符号的 64 位类型。
将result
视为 n×3 矩阵,它必须按列优先顺序存储。
fun()
的主循环中没有printf()
调用。
没有这些功能,我确实得到了预期的 output,即最后一行类似于1.77993e-08 -1.12816e-08 1
或0 0 1
。
尽管这对问题来说并不重要,但我还是对程序的作用做了一个简短的解释,以使其更容易理解。 它以特定排列计算球体表面n
个点的x
、 y
、 z
三维坐标。 z
值 go 从 -1 到 1 以相等的增量,但是,由于数值舍入误差,最后一个值不会恰好为 1。 坐标被写入一个n
× 3 矩阵result
,以列优先顺序存储。 r
和phi
是 (x, y) 平面中的极坐标。
请注意,当z
为-1
或1
时, r
变为 0。这发生在第一个和最后一个迭代步骤中。 这将导致在1.0 / r
表达式中除以 0。 但是, 1.0 / r
被排除在循环的第一次和最后一次迭代之外。
这是由 x87 80 位内部精度的相互作用、GCC 的不一致行为以及编译器版本之间的优化决策不同引起的。
x87 仅支持 IEEE binary32 和 binary64 作为存储格式,在加载/存储时与其 80 位表示形式相互转换。 为了使程序行为可预测,C 标准要求在赋值时放弃额外精度,并允许通过FLT_EVAL_METHOD
宏检查中间精度。 使用-mfpmath=387
时, FLT_EVAL_METHOD
为 2,因此您知道中间精度对应于long double
类型。
不幸的是, GCC 不会降低分配的额外精度,除非您通过-std=cNN
(而不是-std=gnuNN
)请求更严格的一致性,或者明确传递-fexcess-precision=standard
。
在你的程序中, z += 2.0 / (n - 1);
声明应通过以下方式计算:
2.0 / (n - 1)
。z
值(仍然是 80 位精度)。z
的声明类型(即到 binary64) 。在以 NaN 结尾的版本中,GCC 改为执行以下操作:
2.0 / (n - 1)
一次。z
。 这是不符合要求的,因为2.0 / (n - 1)
进行了两次舍入(首先是 binary80,然后是 binary64)。
上面解释了为什么您看到不同的结果取决于编译器版本和优化级别。 但是,通常您不能期望您的计算在最后一次迭代中不产生 NaN。 当n - 1
不是 2 的幂时, 2.0 / (n - 1)
不能精确表示,可能会四舍五入。 在这种情况下,“z”的增长速度可能比真正的和-1.0 + 2.0 / (n - 1) * i
快一点,并且对于i == n - 1
可能最终超过 1.0,导致sqrt(1 - z*z)
由于参数是否定而产生 NaN。
事实上,如果您在程序中将#define N 11
更改为#define N 12
,您将确定性地获得具有 80 位和 64 位中间精度的 NaN。
... NaN 结果是如何产生的(?)
尽管更好地遵守 C 规范显然可以解决 OP 的直接问题,但我断言应该考虑其他预防措施。
当|z| > 1.0
时, sqrt(1 - z*z)
是候选 NaN |z| > 1.0
。
除以零的索引测试预防可能还不够,然后导致cos(INFINITE)
,这是另一种 NaN 可能性。
// /* avoids division by zero when r == 0 */
// if (i != 0 && i != n-1) {
// phi += 1.0 / r;
// }
为了避免这些,1) 直接测试和 2) 使用更精确的方法。
if (r) {
phi += 1.0 / r;
}
// double r = sqrt(1 - z*z);
double rr = (1-z)*(1+z); // More precise than 1 - z*z
double r = rr < 0.0 ? 0.0 : sqrt(rr);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.