浮点相等性意外工作

Floating-point equality unexpectedly working

提问人:k314159 提问时间:1/12/2023 更新时间:1/13/2023 访问量:87

问:

我们经常被教导,浮点数不应该为了完全相等而进行比较。然而,以下函数在传递任何正数时返回黄金比例,实际上确实比较了双倍的相等性,令我惊讶的是,它似乎总是有效:

public static double f(double x) {
    double y;
    while ((y = 1 + 1 / x) != x)
        x = (x + y) / 2;
    return x;
}

@Test
void test() {
    assertEquals((1 + sqrt(5)) / 2, f(1.0));  // Passes!
}

我认为也许它适用于某些输入参数,但不适用于其他参数。但即使我使用 JQwik 的属性测试,它仍然有效!

@Property
void test2(@ForAll @Positive double x) {
    assertEquals((1 + sqrt(5)) / 2, f(x));  // Always passes!
}

谁能告诉我为什么我从来没有遇到过两个浮点数相差很小的情况?

Java 浮点 精度

评论

1赞 k314159 1/12/2023
@ElliottFrisch,当然不是,它被 Java 规范定义为 a。Java 总是返回 ,并且每隔一个数字就会被提升为 double,因为它参与了涉及 .doubleMath.sqrtdoubledouble
1赞 Steve Summit 1/12/2023
这是浮点运算的可悲悖论的一个很好的例子。它并不完美,这意味着它确实存在一些问题,人们总是注意到问题,所以它们隐约可见,很多人都有一种——相当严重的错误——印象,即浮点运算“总是不准确”或“可怕”或“破碎”。但事实上,IEEE-754 标准是经过精心编写的,它旨在在某些不可动摇的约束范围内始终为您提供最佳结果。正如您的代码所展示的那样,它通常工作得很好!
1赞 Steve Summit 1/12/2023
虽然,话虽如此,它并不总是完美地工作,这就是为什么你不能总是期望像你到达这里那样完全平等的结果。(另外,我认为 Java 并没有被完全定义为使用严格的 IEEE-754,所以我的论点有点崩溃。
3赞 Steve Summit 1/12/2023
我认为底线是你——也许是偶然的——选择了一个好的算法,它保证收敛,而且永远不会最终振荡。大多数时候,当你通过这样的无限序列计算结果时,你确实需要使用一个近似相等的终止测试,否则,如果你的循环最终在交替高于和略低于“真实”结果的值中振荡,它可能会永远运行。
1赞 Elliott Frisch 1/12/2023
@SteveSummit 从 JEP-306 和 Java 17 开始,Java 又回到了严格的 IEEE-754 标准,即使在 Intel 上也是如此。howtodoinjava.com/java/new-features

答:

2赞 Henry 1/12/2023 #1

你只是运气好,一般来说,你没有得到完全的平等。例如,尝试以下操作:

public static void main(String[] args) {
    var s = 0.0;
    for (int i = 0; i < 10; i++) {
        s += 0.1;
    }
    System.out.println(s == 1.0);
}

在你的具体例子中,必须进行仔细的分析,以证明你的迭代总是收敛到最接近 phi 的浮点数。如果 sqrt 还返回最接近精确根的浮点数,我们将得到完全相等。

评论

1赞 k314159 1/12/2023
我知道,但真正的问题是“为什么我很幸运”?如果我处理的是可以用浮点二进制精确表示的数字,例如负幂 2,我会期望得到确切的结果。但实际上这个数字是一个无理数。
3赞 Steve Summit 1/12/2023
是的,phi 是无理的,但事实证明,phi′(最接近 phi 的浮点值(由程序计算))正好等于 phi′。:-)
1赞 k314159 1/13/2023
@SteveSummit真的。但令我惊讶的是,两种截然不同的计算 phi 的方法得出了完全相同的近似值。我想我不应该这样做,因为只有一个可能的值是“最接近 phi”。
1赞 chux - Reinstate Monica 1/13/2023 #2

...令我惊讶的是,它似乎总是有效:

并非总是如此。

当我尝试时,和值在两个浮点值之间振荡,这两个浮点值在Summit的最后几位@Steve有所不同。因此是一个无限循环。f(-1/1.6180339887498949)xy

x:-0.61803398874989490  y:-0.61803398874989468   // Decimal notation
x:-0x1.3c6ef372fe950p-1 y:-0x1.3c6ef372fe94ep-1  // Hex notation

x:-0.61803398874989479  y:-0.6180339887498949
x:-0x1.3c6ef372fe94fp-1 y:-0x1.3c6ef372fe950p-1

x:-0.61803398874989490  y:-0.61803398874989468
x:-0x1.3c6ef372fe950p-1 y:-0x1.3c6ef372fe94ep-1

f(some_starting_x)通常收敛以呈现一个 ,使得再次满足停止条件。x1 + 1 / xx

更好的例程可以证明,如果相当接近,循环最终会接近所需的答案,但即便如此,如上所示的振荡也是可能的。因此,需要使用迭代限制足够接近的测试。通常,当 2 个振荡值接近时,它们会被按摩(例如平均)以形成最佳答案。如果不关闭,循环根本无法找到稳定的答案。xwhile


谁能告诉我为什么我从来没有遇到过两个浮点数相差很小的情况?

测试不充分。


故事的士气

不要只依赖浮点相等,除非在特定情况下。
不是选择案例,应该得到额外的停止代码。
f()


Ref: 两个具有数学属性: :xx = 1 + 1/x

x1 = 1.6180339887498948482045868343656...
x2 = -0.61803398874989484820458683436564...

注意。 是Golden_ratio φx1*x2 == -1x1