浮点数里的 NAN
起因
最近在给一个 Scheme 语言写测试,要测的函数是 positive?
和 negative?
,很简单,就是判断一个实数的正负。为了使测试覆盖到更多的情况,在测试用例里特别加了 NaN
,根据文档里的说法,应该都返回 #f
即 false
才对。本地测试也没问题,然而发了 PR 后 CI 里的测试结果确有问题,linux 和 mac 的测试环境里能跑过,windows 上有一个测试一直出错,错误的地方如下:
(positive? 0.0) => #t
(negative? 0.0) => #f
而在mac上是这样的:
(positive? +nan.0) => #f
(negative? +nan.0) => #f
调查
发现这个问题后我首先有了两个想法:会不会是 NaN
的值在不同平台表示形式是不一样的,导致比较大小的结果不一样?为什么在 windows 上会显示 0.0
而不是 +nan.0
?
针对显示为 0.0
的问题,我先用了更多的用例进行测试,对比 mac 和 windows 的结果来判断是否把 NaN
当成 0
来输出了:
(> 0.0 +nan.0)
(< 0.0 +nan.0)
(= 0.0 +nan.0)
(> 0.0 0.0)
(< 0.0 0.0)
(= 0.0 0.0)
在 mac 和 windows 上的结果如下:
code | mac | windows |
---|---|---|
(> 0.0 +nan.0) | #f |
#t |
(< 0.0 +nan.0) | #f |
#f |
(= 0.0 +nan.0) | #f |
#t |
(> 0.0 0.0) | #f |
#f |
(< 0.0 0.0) | #f |
#f |
(= 0.0 0.0) | #t |
#t |
通过前三行和后三行的对比,可知程序中并没有把 NaN
当作 0.0
在操作。下面就来研究为什么会显示成 0.0
,这部分源码在 s7.c 中的函数 dtoa_filter_special
static int32_t dtoa_filter_special(double fp, char *dest, bool neg)
{
uint64_t bits;
bool nan;
if (fp == 0.0)
{
dest[0] = '0'; dest[1] = '.'; dest[2] = '0';
return(3);
}
bits = dtoa_get_dbits(fp);
nan = (bits & dtoa_expmask) == dtoa_expmask;
if (!nan) return(0);
......
在本地调试的时候发现,传进来的 fp
的值就是 NaN
(也就是说之前第一个想法是错的),然后会判断是否和 0
相等,如果等于,就会把要输出的字符串 dest
设为 "0.0
",也就是报错机器上显示的结果。
这样看来,在报错机器上确实存在 NaN
和 0
比较结果"不对"的情况,而 positive?
和 negative?
也是通过与 0 判断实现的,因此也受到了影响。
从上面的测试可以看出 NaN
即小于 0
又等于 0
,这样的数肯定是不存在的。再试几个其它数与 NaN
比较,发现任意非正实数都会比 NaN
大。那究竟是什么导致了这个结果呢?
这时我已经没思路了,后来项目上的另一个师兄后来发现,是 vc 的编译选项导致的(/fp Specify floating-point behavior)。在CI里用了 /fp:fast
所以结果不正确,改成 /fp:precise
三个平台测试结果就一致没问题了。
继续研究
IEEE754 中其实说明了 NaN
在浮点数中有相对应的表示,比如在我本地测试的时候,它的值为 0x7ff8000000000000。在上面的转字符串代码中,也使用了相应的方法判断是否是 NaN:nan = (bits & dtoa_expmask) == dtoa_expmask
那为什么会出现不一样的结果呢?首先看看不同编译选项生成的汇编代码是否相同,这里对比了好久也没发现异样。幸好在另一位大佬的帮助下才发现原来问题就出在不同的汇编代码,之前眼花没发现。。比如:
NaN<0
的结果不同,因为二者汇编代码分别为:
- 默认情况(/fp:precise)
xorps xmm0, xmm0
comisd xmm0, mmword ptr [NaNValue (rbp+8)]
jbe main+0x41
- /fp:fast 的情况
movsd xmm0, mmword ptr [NaNValue (rbp+8)]
comisd xmm0, mmword ptr [real@0000000000000000]
jae main+0x46
使用了不同比较方式和判断指令
NaN==0
结果不同,因为二者汇编代码分别为:
- 默认情况
movsd xmm0, mmword ptr [NaNValue (rbp+8)]
ucomisd xmm0, mmword ptr [real@0000000000000000]
jp main+0x6f
jne main+0x6f
- /fp:fast 的情况
movsd xmm0, mmword ptr [NaNValue (rbp+8)]
ucomisd xmm0, mmword ptr [real@0000000000000000]
jne main+0x72
多了一个判断指令
也就是说,不管在哪种编译选项下,汇编指令运行得到的结果都是一致的,而是编译器通过生成不同的指令而达到令 NaN 比较结果不一样的效果