示例代码:
#include <iostream>
#include <cmath>
#include <stdint.h>
using namespace std;
static bool my_isnan(double val) {
union { double f; uint64_t x; } u = { val };
return (u.x << 1) > (0x7ff0000000000000u << 1);
}
int main() {
cout << std::isinf(std::log(0.0)) << endl;
cout << std::isnan(std::sqrt(-1.0)) << endl;
cout << my_isnan(std::sqrt(-1.0)) << endl;
cout << __isnan(std::sqrt(-1.0)) << endl;
return 0;
}
在线编译器。
利用-ffast-math
;0,0,1,1"——如果没有;1,1,1";。
这是正确的吗?我认为std::isinf
/std::isnan
在这些情况下仍然应该与-ffast-math
一起工作。
此外,如何使用-ffast-math
检查无穷大/NaN?您可以看到my_isnan
在做这件事,它确实有效,但该解决方案当然非常依赖于体系结构。此外,为什么my_isnan
在这里工作,而std::isnan
不工作?那么__isnan
和__isinf
呢。他们总是工作吗?
对于-ffast-math
,std::sqrt(-1.0)
和std::log(0.0)
的结果是什么。它是未定义的,还是应该是NaN/-Inf?
相关讨论:(GCC)[Bug libstdc++/50724]新增:是否仅在g++中被finite数学打破,(Mozilla)Bug 416287-isnan 的性能改进机会
注意-ffast-math
可能会使编译器忽略/违反IEEE规范,请参阅http://gcc.gnu.org/onlinedocs/gcc-4.8.2/gcc/Optimize-Options.html#Optimize-选项:
除了-Ofast之外,任何-O选项都不会打开此选项,因为它可能导致依赖于用于数学函数的IEEE或ISO规则/规范的实现。然而,它可能会为不需要的程序生成更快的代码这些规范的保证。
因此,使用-ffast-math
不能保证在应该看到的地方看到无穷大。
特别是,-ffast-math
开启-ffinite-math-only
,请参阅http://gcc.gnu.org/wiki/FloatingPointMath意思是(从http://gcc.gnu.org/onlinedocs/gcc-4.8.2/gcc/Optimize-Options.html#Optimize-选项)
[…]浮点运算优化,假设参数和结果不是NaNs或+-Infs
这意味着,通过启用-ffast-math
,您向编译器承诺您的代码永远不会使用无穷大或NaN,这反过来又允许编译器优化代码,例如,用常量false
替换对isinf
或isnan
的任何调用(并从中进一步优化)。如果你违背了对编译器的承诺,编译器就不需要创建正确的程序。
因此,答案很简单,如果你的代码可能有无穷大或NaN(这是由你使用isinf
和isnan
的事实强烈暗示的),你就不能启用-ffast-math
,否则你可能会得到错误的代码。
my_isnan
的实现可以工作(在某些系统上),因为它直接检查浮点数的二进制表示。当然,处理器仍然可能进行(一些)实际计算(取决于编译器所做的优化),因此实际的NaN可能会出现在内存中,您可以检查它们的二进制表示,但如上所述,std::isnan
可能已被常量false
取代。同样可能发生的情况是,编译器将sqrt
替换为甚至不为输入-1
生成NaN的某个版本。为了查看编译器进行了哪些优化,请编译到汇编程序并查看代码。
为了进行一个(并非完全无关的)类比,如果你告诉编译器你的代码是用C++编写的,你就不能指望它正确地编译C代码,反之亦然(有一些实际的例子,例如,在C和C++中都有效的代码在用每种语言编译时会产生不同的行为吗?)。
启用-ffast-math
并使用my_isnan
是个坏主意,因为这会使一切都非常依赖于机器和编译器。你不知道编译器总体上做了什么优化,所以可能还有其他隐藏的问题,因为你使用的是非有限数学,但告诉编译器其他的。
一个简单的修复方法是使用-ffast-math -fno-finite-math-only
,它仍然会提供一些优化。
也可能是你的代码看起来像这样:
- 过滤掉所有的无穷大和NaN
- 对过滤后的值进行一些有限的数学运算(我指的是保证永远不会创建无穷大或NaN的数学运算,这必须经过非常仔细的检查)
在这种情况下,您可以拆分代码,并使用优化#pragma
或__attribute__
为给定的代码段选择性地打开和关闭-ffast-math
(分别为-ffinite-math-only
和-fno-finite-math-only
)(然而,我记得与此相关的GCC的某些版本存在一些问题),或者只需将代码拆分为单独的文件,并使用不同的标志进行编译。当然,如果可以隔离可能出现无穷大和NaN的部分,这也适用于更通用的设置。如果您不能隔离这些部分,这强烈表明您不能将-ffinite-math-only
用于此代码。
最后,重要的是要理解-ffast-math
并不是一个无害的优化,它只是让你的程序更快。它不仅会影响代码的性能,还会影响代码的正确性(除此之外,如果我没记错的话,William Kahan在他的主页上有一系列恐怖故事,请参阅每个程序员都应该知道浮点运算的内容)。简而言之,您可能会得到更快的代码,但也会得到错误或意外的结果(请参阅下面的示例)。因此,只有当你真正知道自己在做什么,并且你已经绝对确定
- 优化不会影响特定代码的正确性,或者
- 优化引入的错误对代码来说并不重要
根据是否使用此优化,程序代码的行为实际上可能截然不同。特别是,当启用诸如-ffast-math
之类的优化时,它可能会表现出错误(或者至少与您的预期非常相反)。以以下程序为例:
#include <iostream>
#include <limits>
int main() {
double d = 1.0;
double max = std::numeric_limits<double>::max();
d /= max;
d *= max;
std::cout << d << std::endl;
return 0;
}
在没有任何优化标志的情况下编译时,将按预期产生输出1
,但使用-ffast-math
时,它将输出0
。