计算化学公社

标题: 一个与编译器机制有关的bug:NaN,fast-math [打印本页]

作者
Author:
coolrainbow    时间: 2021-6-17 06:28
标题: 一个与编译器机制有关的bug:NaN,fast-math
本帖最后由 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。所以在编写复杂程序时,可以先关闭不安全的编译选项进行测试,最后再使用激进优化,以保证程序的安全和正确。




作者
Author:
abin    时间: 2021-6-17 08:46
对应的,在intel编译器套件中,
如果使用-O3来优化,
也需要做类似的处理。在intel手册中有提及。

看到很多网友推荐的vasp或者cp2k编译中,仅仅单独使用了-O3,
这是非常危险的。
根据intel手册描述,
“-O3 Enables more aggressive loop and memory access optimizations— such as scalar replacement—loop unrolling, code replication to eliminate branches, loop blocking to allow more efficient use of cache and additional data prefetching. This level of optimization also includes a setting akin to GNU’s -ffast-math and so comes with all the attendant warnings about IEEE floating- point compliance.”

换成白话文就是,英特尔工具链,
仅仅使用-O3,可能带来数值错误。
作者
Author:
snljty    时间: 2021-6-17 08:53
这个feature是否出现似乎还和编译编译器时候的选项、环境等有关,刚试了几个版本,某个版本下加-ffast-math不加-fno-finite-math-only,C语言的程序,gcc编译,isnan(x)没有问题,x != x结果就不对。换个版本,现象又不一样了。
作者
Author:
coolrainbow    时间: 2021-6-17 09:46
snljty 发表于 2021-6-17 08:53
这个feature是否出现似乎还和编译编译器时候的选项、环境等有关,刚试了几个版本,某个版本下加-ffast-math ...

是的,在Windows的Mingw C++和Linux下的g++也会表现不同,所以看来跟具体实现有关。总之这种危险的优化一定要多多测试。
作者
Author:
coolrainbow    时间: 2021-6-17 09:53
abin 发表于 2021-6-17 08:46
对应的,在intel编译器套件中,
如果使用-O3来优化,
也需要做类似的处理。在intel手册中有提及。

数值错误还可以通过O0之类的来排查,有时候gnu能编译的,intel就完全编译不过去:让人一点脾气也没有
作者
Author:
abin    时间: 2021-6-17 10:20
本帖最后由 abin 于 2021-6-17 10:22 编辑
coolrainbow 发表于 2021-6-17 09:53
数值错误还可以通过O0之类的来排查,有时候gnu能编译的,intel就完全编译不过去:让人一点脾气也没有

是呀,
比如libxc v5.1.4GNU编译没有任何问题。
换到intel,就各种错误。
好不容易搞过去了, 结果cp2k测试算例,一堆错误。

头疼。

作者
Author:
sobereva    时间: 2021-6-17 12:09
-O3我都不用,比起-O2提升很有限或者几乎没任何提升(取决于代码),但却会带来很多危险

我写程序一般都是先用debug模式跑一遍。有些-O1/-O2状态下不会报错的危险情况在debug情况下都会报错


作者
Author:
lyj714    时间: 2021-6-17 13:14
本帖最后由 lyj714 于 2021-6-17 13:28 编辑

mark
作者
Author:
coolrainbow    时间: 2021-6-18 10:17
sobereva 发表于 2021-6-17 12:09
-O3我都不用,比起-O2提升很有限或者几乎没任何提升(取决于代码),但却会带来很多危险

我写程序一般都 ...

根据经验,gnu系列的,gfortran,gcc的O3还是可靠的

intel的O3个别情况下还没O0快,不少时候编译都不过,必须改成O2。 老bug了。




欢迎光临 计算化学公社 (http://bbs.keinsci.com/) Powered by Discuz! X3.3