|
|
本帖最后由 coolrainbow 于 2021-6-17 06:39 编辑
今天本人在写程序时遇上了一个难以发现的bug。经过层层函数的插桩最后发现与NaN以及编译器的机制有关,特此记录以供参考。
(1) 什么是NaN
NaN是IEEE754浮点数运算标准中的一个定义。通俗的说,一个实数范围内无意义的计算被定义为返回一个“非数”,即NaN (Not a number),比如负数开平方,负数取对数,绝对值大于1的数取反三角函数等。NaN有个奇异的规定,即它不等于自己,也就是说NaN != NaN。因此,它可以用来判断一个实数是否是非数。比如一个real(kind=4) 或者double 型的x , x==x为真,则x是个普通的实数;否则,它就是NaN。另外,Fortran和C++中都有个专门的函数 isnan() 来判断是否为NaN。
(2) 代码bug
我遇到的NaN的bug可以简化成如下的Fortran代码(nan.f90):
program main
real(kind=4) x
x = -1
x = x**(0.5)
write (*, *) "x = ",x
if (x == x) then
write (*, *) "x is an actual number."
else
write (*, *) "x is not a number."
endif
end
可以预见,因为x是个负数开平方,所以最后应该得到一个NaN,并且输出“x is not a number.”。 简单编译运行后确实如此:
$ gfortran nan.f90
$ ./a
x = NaN
x is not a number.
然而,在我的程序中,为了程序的运行效率,添加了一个激进的、不安全的编译选项:fast-math。顾名思义,就是快速的数学运算。如果你编译过NAMD,就会发现fast-math是一个默认的编译选项,可以极大的提升计算速度。在我这里也是如此。然而,用这个选项编译上面的程序,却带来了错误的运行结果:
$ gfortran nan.f90 -ffast-math
$ ./a
x = NaN
x is an actual number.
x明明已经是NaN了,为什么下面却判断错了?难道是编译器的bug?
当然不是。虽然intel编译器的bug满天飞,但是GNU编译器发展到现在已经无比稳定,一般只有在极其复杂的C++语法中才能发现bug,Fortran编译器发现bug的概率极低。经过查阅资料,这才发现:fast-math为了提升效率,会假定所有的浮点是都是实数,不会出现NaN,Inf之类的情况。所以isnan()函数和x==x这类技巧都会失效。这就是NaN判断失败的原因!
bug原因找到了也就好解决了。编译器手册指出,加上一个选项no-finite-math-only就可以撤销只有实数的假定。
$ gfortran nan.f90 -ffast-math -fno-finite-math-only
$ ./a
x = NaN
x is not a number.
(3) 总结
这个特征在GNU的gfortran,gcc,g++编译器都存在,解决方法就是在fast-math后面加no-finite-math-only。由此可见,在程序中使用不安全的优化选项时一定要慎重,虽然大多数情况下没什么问题,但一旦有问题就是难以排查的bug。所以在编写复杂程序时,可以先关闭不安全的编译选项进行测试,最后再使用激进优化,以保证程序的安全和正确。
|
评分 Rate
-
查看全部评分 View all ratings
|