-Ofast 在使用 long double 时产生不正确的代码

-Ofast produces incorrect code while using long double

#include <cstdio>

int main(void)
{
    int val = 500;
    printf("%d\n", (int)((long double)val / 500));
    printf("%d\n", (int)((long double)500 / 500));
}

显然应该输出1 1。但是如果用-Ofast编译的话,会输出0 1,为什么?

而如果你把500改成其他值(比如400)用-Ofast编译,还是会输出1 1.

带有 -Ofast 的编译器资源管理器:https://gcc.godbolt.org/z/YkX7fB

似乎是这一行引起了问题。

-Ofast

Disregard strict standards compliance. -Ofast enables all -O3 optimizations. It also enables optimizations that are not valid for all standard-compliant programs. It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific [...]

-ffast-math

Sets the options -fno-math-errno, -funsafe-math-optimizations, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans, -fcx-limited-range and -fexcess-precision=fast.

This option causes the preprocessor macro __FAST_MATH__ to be defined.

This option is not turned on by any -O option besides -Ofast since it can result in incorrect output for programs that depend on an exact implementation of IEEE or ISO rules/specifications for math functions. It may, however, yield faster code for programs that do not require the guarantees of these specifications.

结论:不要使用-ffast-math除非你愿意得到像现在这样的惊喜。

使用 -Ofast 时,启用 -ffast-math,这会导致某些操作以不同且更快的方式计算。在您的情况下,(long double)val / 500) 可以计算为 (long double)val * (1.0L / 500))。当您比较以下函数的 -O2-Ofast 时,可以在生成的程序集中看到这一点:

long double f(long double a)
{
    return a / 500.0L;
}

-O2生成的程序集涉及fdiv指令,而-Ofast生成的程序集涉及fmul指令,见https://gcc.godbolt.org/z/58VHxb.

其次,1/500,即0.002,并不完全可以用long double表示。因此,会发生一些舍入,而且在您的情况下,这种舍入似乎是向下的。这可以通过以下表达式进行检查:

500.0L * (1.0L / 500.0L) < 1.0L

计算为truehttps://gcc.godbolt.org/z/zMcjxJ。因此,精确存储的乘数是 0.002 - 一些非常小的增量

最后乘法的结果是500 * (0.002 - delta) = 1 - 某个小值。当此值转换为 int 时,它被截断,因此 int 中的结果为 0。

即使显示的程序片段有一个 'problem',无论如何它都是错误的处理浮点数的方法。

您或多或少地询问程序浮点数是否具有 'exact value' - 在本例中为“1”。好的,更准确地说 - 如果值为 'around' 1 的值为 '< 1' 或 '>= 1' - 那么恰好在两个答案的分界线附近。但是正如其他人已经写过的(或者可以很容易地在维基百科中找到,...)浮点数的精度有限。所以这样的偏差可能而且将会发生。

因此,得出一个结论:在将浮点数转换为整数时,您应该始终使用舍入,即“(int) round (floating_point_value)”。

PS。与其他人可能所说或推荐的相反 - 我根本没有看到 -ffast-math 计算有任何问题。唯一的 'problem' 是在不同计算机上 运行 之后(按位)比较某个程序的结果。

我使用 -ffast-math(实际上是 -Ofast)进行所有(科学)计算。但到目前为止,这从来都不是问题——因为我预计浮点数会有一些舍入误差(这是真的,无论是否使用 -ffast-math 或不)——但据我所知,仅此而已。由于我通常使用 64 位浮点数(双精度),这意味着计算精确到大约 15 到 17 位十进制数字 - 最后(少数)它们受到这些不准确的影响 - 仍然给我很多 'accurate' 数字- 比如说 - 超过 13 个,这取决于我的计算有多复杂。