是否可以假设浮点相等和不等测试是一致的和可重复的?

Can floating point equality and inequality tests be assumed to be consistent and repeatable?

假设有两个浮点数 x 和 y,它们都不是 Nan。

是否可以安全地假设平等和不平等测试将:

一个。彼此保持一致: 例如。以下保证为真: (x >= y) == ((x > y) || (x == y))

b。可重复 例如。如果 x 和 y 都没有改变,那么每次计算时 (x == y) 总是会给出相同的结果。 我问这个问题是因为浮点单元可以将中间结果存储到比它们存储在内存中更高的精度,因此变量的值可能会发生变化,具体取决于它是否是最近计算的结果仍在 FPU 中,还是来自内存。可以想象,第一个测试可能是前者,稍后测试后者。

不管 C++ 标准如何,这种不一致在实践中会在各种设置中发生。

有两个例子很容易触发:

对于 32 位 x86,情况就不太好了。欢迎使用 gcc bug number 323,因为 32 位应用程序不符合标准。发生的情况是 x86 的浮点寄存器有 80 位,无论程序中的类型如何(C、C++ 或 Fortran)。这意味着以下通常是比较 80 位值,而不是 64 位值:

bool foo(double x, double y) 
{
     // comparing 80 bits, despite sizeof(double) == 8, i.e., 64 bits
     return x == y;
}

如果 gcc 可以保证 double 总是占用 80 位,这将不是什么大问题。不幸的是,浮点寄存器的数量是有限的,有时值存储在(溢出到)内存中。因此,对于相同的 x 和 y,x==y 在溢出到内存后可能计算为 true,而在没有溢出到内存的情况下计算为 false。无法保证(缺乏)溢出到内存。行为会根据编译标志和看似不相关的代码更改而随机变化。

因此,即使 x 和 y 在逻辑上应该相等,并且 x 被溢出,x == y 也可能计算为 false,因为 y 包含一个 1 位在尾数的最低有效位中,但 x 由于溢出而截断了该位。那么你的第二个问题的答案是,x ==y 在 32 位 x86 程序中可能 return 不同的结果基于溢出或缺失。

类似地,x >= y 可能 return true,即使 y 应该略大于 x。如果在溢出到内存中的 64 位变量后,值变得相等,就会发生这种情况。在这种情况下,如果在代码 x > y || x == y 的前面计算而没有溢出到内存,那么它将计算为 false。更令人困惑的是,将一个表达式替换为另一个表达式可能会导致编译器生成略有不同的代码,并导致不同的内存溢出。对于两个表达式,溢出的差异可能最终会给出不一致的不同结果。

同样的问题可能发生在任何以不同宽度(例如 32 位 x86 的 80 位)执行浮点运算的系统中,而不是代码想要的(64 位)。解决这种不一致的唯一方法是在每次浮点运算后强制溢出,以截断多余的精度。由于性能下降,大多数程序员并不关心这一点。

第二种可能引发不一致的情况是不安全的编译器优化。许多商业编译器默认将 FP 一致性排除在 window 之外,以便获得几个百分点的执行时间。编译器可能会决定更改 FP 操作的顺序,即使它们可能会产生不同的结果。例如:

v1 = (x + y) + z;
v2 = x + (y + z);
bool b = (v1 == v2);

显然很可能 v1 != v2,因为四舍五入不同。例如,如果 x == -yy > 1e100z == 1,则 v1 == 1v2 == 0。如果编译器过于激进,那么它可能会简单地考虑代数并推断出 b 应该是 true,甚至没有评估任何东西。这是 运行 gcc -ffast-math.

时发生的情况

Here is 一个展示它的例子。

这种行为会使 x == y 变得不一致,并且在很大程度上取决于编译器在特定代码段中可能推断出的内容。

如果问题中的xy是标识符(而不是一般表达式的缩写,例如x代表b + sqrt(c)),那么C++标准要求 (x >= y) == (x > y || x == y) 为真。

C++ 2017(草案 N4659)8 13 允许以比标称类型所需的精度和范围更高的精度和范围对浮点表达式求值。例如,在评估具有 float 个操作数的运算符时,实现可能会使用 double 算术。但是,那里的脚注 64 让我们参考 8.4、8.2.9 和 8.18 以了解强制转换和赋值运算符必须执行它们的特定转换,这会产生一个可表示为标称类型的值。

因此,xy一旦被赋值,就没有多余的精度,它们在不同的用途中也没有不同的值。那么 (x >= y) == (x > y || x == y) 必须为真,因为它是按照它出现的方式求值的,并且在数学上必然为真。

GCC bug 323的存在意味着在为i386 编译时不能依赖GCC,但这是由于GCC 中的一个bug 违反了C++ 标准。标准 C++ 不允许这样做。

如果在表达式之间进行比较,如:

double y = b + sqrt(c);
if (y != b + sqrt(c))
    std::cout << "Unequal\n";

那么分配给 y 的值可能与为 b + sqrt(c) 的右运算符计算的值不同,并且可能会打印字符串,因为 b + sqrt(c) 可能具有过高的精度,而 y 不能。

由于还需要转换来去除多余的精度,因此 y != (double) (b + sqrt(c)) 应该始终为假(给定上面 y 的定义)。