在GNU C中,您可以将assert(x*x >= 0.f)
编写为编译时承诺,而不是运行时检查,如下所示:
#include <cmath>
float test1 (float x)
{
float tmp = x*x;
if (!(tmp >= 0.0f))
__builtin_unreachable();
return std::sqrt(tmp);
}
(相关: __builtin_unreachable有哪些优化? 你也可以将if(!x)__builtin_unreachable()
封装在宏中,并将其命名为promise()
或其他名称。)
但是gcc不知道如何利用
tmp
非NaN和非负的承诺。我们仍然会得到(
Godbolt)相同的预设汇编序列,检查
x>=0
,否则调用
sqrtf
设置
errno
。
假定将这个展开成比较和分支发生在其他优化传递之后,因此让编译器了解更多并没有帮助。
这是一个错失优化的逻辑,当启用
-fmath-errno
(默认情况下启用)时,它会在内联
sqrt
时进行推测。您需要的是
-fno-math-errno
,这在全局范围内是安全的。
如果您从不依赖于数学函数设置
errno
,则
这是100%安全的。没有人想要那样,这就是NaN传播和/或记录掩码FP异常的粘性标志所用。例如,通过
#pragma STDC FENV_ACCESS ON
访问C99/C++11
fenv
,然后使用
fetestexcept()
等函数。请参见
feclearexcept
中的示例,其中显示了使用它来检测除以零。
FP环境是线程上下文的一部分,而
errno
是全局的。
支持这种过时的功能并非免费;除非您有使用它的旧代码,否则应将其关闭。不要在新代码中使用它:请使用
fenv
。理想情况下,对于
-fmath-errno
的支持应尽可能便宜,但由于几乎没有人实际使用
__builtin_unreachable()
或其他排除NaN输入的方法,因此开发人员没有实现优化,这很可能是不值得的。不过,如果您愿意,可以报告未优化错误。
实际的FPU硬件确实具有这些粘性标志,这些标志保持设置直到被清除,例如x86的mxcsr
状态/控制寄存器用于SSE/AVX数学,或者其他ISA中的硬件FPU。在FPU可以检测异常的硬件上,优质的C++实现将支持像fetestexcept()
这样的内容。如果不支持,则math-errno
可能也无法正常工作。
数学方面的
errno
是一个旧的过时设计,C/C++仍然默认使用它,但现在被广泛认为是一个坏主意。这使得编译器难以高效地内联数学函数。或者也许我们没有像我想的那样被它牢牢束缚:
为什么即使sqrt取出域参数,errno也没有设置为EDOM?解释了在ISO C11中,在数学函数中设置errno是
可选的,实现可以指示他们是否这样做。大概在C++中也是这样。
将-fno-math-errno
与改变值的优化(如-ffast-math
或-ffinite-math-only
)混为一谈是一个大错误。你应该强烈考虑全局启用它,或者至少对包含此函数的整个文件启用它。
float test2 (float x)
{
return std::sqrt(x*x);
}
test2(float):
mulss xmm0, xmm0
sqrtss xmm0, xmm0
ret
如果你永远不会使用
feenableexcept()
来取消屏蔽FP异常,那么最好也使用
-fno-trapping-math
。 (虽然这个选项对于此优化并不是必需的,只有设置
errno
的问题才是问题所在)。
-fno-trapping-math
不假设没有NaN或其他任何东西,它只假定FP异常(如无效或不精确)永远不会实际调用信号处理程序,而是产生NaN或舍入结果。
-ftrapping-math
是默认值,但
据GCC开发人员Marc Glisse称,它已经坏掉了并且“从未工作过”。(即使打开它,GCC也会进行一些优化,这些优化可以将引发的异常数量从零变为非零或反之。它还阻止了一些安全优化)。但不幸的是,
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192(默认情况下关闭)仍然存在。如果您确实取消了异常屏蔽,则最好使用
-ftrapping-math
,但再次强调,很少需要这样做,而只需在某些数学运算后检查标志或检查NaN即可。而且它实际上也不能保留精确的异常语义。
参见SIMD for float threshold operation,其中一个案例中,-ftrapping-math
默认设置错误地阻止了一项安全优化。(即使将可能触发异常的操作提升到 C 无条件执行,gcc 生成的非向量化汇编代码仍然有条件地执行!因此,GCC不仅阻止了向量化,还改变了异常语义与C抽象机器的不同。)-fno-trapping-math
启用了预期的优化。
x*x
不一定是零或正数,它可能是一个 NaN。虽然我不确定 GCC 在此处理的是否为 NaN。 - Eric Postpischil-fno-math-errno
是更安全的选项,同时也会移除对sqrtf
的调用。 - haroldsqrtf
在出现错误时设置了errno
,那么GCC正确地不使用sqrtss
来处理NaN情况。(根据C标准,数学函数是否设置errno
是与实现相关的,而C++继承了这一点。) - Eric Postpischil-ffinite-math-only
告诉GCC可以假设不存在无穷大或NaN。使用此选项可以消除分支和对sqrtf
的调用。由于无穷大不是sqrtf
的错误,因此这证实了GCC在问题示例代码中的担忧是一个NaN。不幸的是,我没有看到一个选项可以仅仅假设没有NaN,而不是假设没有NaN或无穷大;在sqrt
之前插入if (std::isnan(x)) return x;
并不能导致GCC意识到x*x
不可能是一个NaN。 - Eric Postpischil