ᕦʕ •ᴥ•ʔᕤ

浮点数里的 NAN

起因

最近在给一个 Scheme 语言写测试,要测的函数是 positive?negative? ,很简单,就是判断一个实数的正负。为了使测试覆盖到更多的情况,在测试用例里特别加了 NaN ,根据文档里的说法,应该都返回 #ffalse 才对。本地测试也没问题,然而发了 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",也就是报错机器上显示的结果。

这样看来,在报错机器上确实存在 NaN0 比较结果"不对"的情况,而 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 的结果不同,因为二者汇编代码分别为:

xorps   xmm0, xmm0
comisd  xmm0, mmword ptr [NaNValue (rbp+8)]
jbe     main+0x41
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
movsd   xmm0, mmword ptr [NaNValue (rbp+8)]
ucomisd xmm0, mmword ptr [real@0000000000000000]
jne     main+0x72

多了一个判断指令

也就是说,不管在哪种编译选项下,汇编指令运行得到的结果都是一致的,而是编译器通过生成不同的指令而达到令 NaN 比较结果不一样的效果