Go浮点数比较

14

如果要在Go中比较两个浮点数(float64)是否相等,我的初步理解是通过IEEE 754标准和浮点数的二进制表示来实现:

func Equal(a, b float64) bool {
    ba := math.Float64bits(a)
    bb := math.Float64bits(b)
    diff := ba - bb
    if diff < 0 {
        diff = -diff
    }
    // accept one bit difference
    return diff < 2
}

问题是:相对于旧的 abs(diff) < epsilon 方法,这是一种更通用、更精确、更高效的方法来比较两个任意大或小的浮点数是否“几乎相等”吗?我的想法是,如果在二进制表示中只允许有一位差异,那么被比较的数字肯定不能再相等了,除非是严格相等,而严格相等(如评论中所指出)可以用浮点数的==进行检查。

注意:我已经编辑了问题以使其更加清晰。


你不能简单地做 a == b 吗?请注意,固定大小的浮点数值很容易无法准确表示您想要表示的值,例如值 0.10.3。您最好的选择可能是像大多数编程语言一样,从一个数中减去另一个数,进行 ABS 操作并将其与您认为“足够接近以便于我的目的相等”的阈值进行比较。 - Lasse V. Karlsen
但是,再次问一下,为什么仅使用 a == b 不对?请解释一下为什么这对您不是一个选项。 - Lasse V. Karlsen
阈值的问题在于,对于非常小或非常大的数字,阈值要么比您实际比较的数字更小,要么更大。 - augustzf
这是正确的,但在给定的领域中,将非常小的值与非常大的值组合在固定大小的浮点数中而不知道它是不典型的。但是,既然您已经尝试实现浮点值的位相等性,那么a == b有什么问题呢? - Lasse V. Karlsen
2
请搜索一下每个程序员都应该了解的浮点数知识。0.1+0.2是一个经典的例子。 - Volker
显示剩余2条评论
3个回答

41
不要使用 float64 的位表示法,因为在很多情况下这是没有意义的。只需相减即可找出两个数字的差异:
package main

import (
    "fmt"
    "math"
)

const float64EqualityThreshold = 1e-9

func almostEqual(a, b float64) bool {
    return math.Abs(a - b) <= float64EqualityThreshold
}

func main() {
    a := 0.1
    b := 0.2
    fmt.Println(almostEqual(a + b, 0.3))
}

4
@augustzf http://floating-point-gui.de/errors/comparison/#look-out-for-edge-cases - kostix
1
@augustzf 你可以使用类似于 math.Abs(a - b) <= float64EqualityThreshold * (math.Abs(a) + math.Abs(b)) 的东西。@kostix 的链接似乎更好。 - Arman Ordookhani
1
@augustzf:没有好的通用比较方法,也不可能有好的通用比较方法 - Eric Postpischil
2
@EricPostpischil,我理解你的意思,但“没有好的通用方法”对我来说有点过了。简单的几乎相等函数可以在大多数情况下使用。如果有人不能容忍聚合的微小误差,那么他/她根本不应该使用浮点计算。 - Arman Ordookhani
@ArmanOrdookhani:它们可以使用,但并不好。大多数关于浮点数相等比较的Stack Overflow问题都是由于某人在进行浮点数学习练习时进行实验引起的。它们没有真正的用途。我唯一记得的其他情况是单元测试。 (我在这里回答了一个问题。)在这种情况下,我们可以限制由于浮点舍入而产生的误差,并相信它低于由于错误而产生的误差,因此我们可以使用明确的值来作为公差,以进行有用的比较。 - Eric Postpischil
3
除此之外,展示“方法”进行浮点数比较的答案通常使用不好的例子。它们使用任意的容差,这些容差通常不适用,有时会不正确地使用FLT_EPSILONDBL_EPSILON(例如,未按涉及数字的数量级进行缩放,这只是其中一个错误),任意在绝对误差和相对误差之间切换,因为作者想出了一些基本可行的东西,而不是基于任何操作理论,等等。它们是巧妙的解决方案,而不是好的通用解决方案。 - Eric Postpischil

32

不,这不是比较浮点数的正确方式。

您并没有实际说明您的真正问题——您正在尝试比较两个浮点数,但您没有说是什么原因。

浮点运算旨在执行近似算术。浮点运算中会有舍入误差的积累是正常的。当以不同的方式计算值时,这些误差通常会不同,因此不应期望浮点运算产生相等的结果。

在您的示例中,进行了以下运算:

  • 十进制数字“0.1”被转换为float64(IEEE-754 64位二进制浮点数)。这产生了值0.1000000000000000055511151231257827021181583404541015625,这是最接近0.1的float64值。

  • 十进制数字“0.2”被转换为float64。这产生了值0.200000000000000011102230246251565404236316680908203125,这是最接近0.2的float64值。

  • 它们被相加。这产生了0.3000000000000000444089209850062616169452667236328125。除了当0.1和0.2 舍入到float64中最接近的值时发生舍入误差外,这还包含了一些额外的舍入误差,因为无法在float64中表示精确的和。

  • 十进制数字“0.3”被转换为float64。这产生了值0.299999999999999988897769753748434595763683319091796875,这是最接近0.3的float64值。

如您所见,将0.10.2相加的结果与0.3积累了不同的舍入误差,因此它们是不相等的。没有正确的相等性测试会报告它们相等。此外,重要的是,在此示例中发生的错误是特定于此示例的 - 不同的浮点操作序列将具有不同的错误,并且积累的错误不仅限于数字的低位

一些人尝试通过测试差异是否小于某个小值来进行比较。在某些应用程序中可能可以,但在您的应用程序中可以吗?我们不知道您正在尝试做什么,因此我们不知道会发生什么问题。允许小误差的测试有时会报告不正确的结果,无论是虚报(因为它们接受用精确数学计算时不相等的数字作为相等的数字)还是漏报(因为它们拒绝对用精确数学计算时相等的数字进行相等性判断)。对于您的应用程序,哪一个错误更糟糕?其中一个会导致机器损坏或人员受到伤害吗?如果不知道这一点,没有人能建议哪种不正确的结果是可以接受的,甚至是否存在这样的结果。

此外,误差公差应该设置多大?在计算中可能发生的总体误差取决于执行的操作序列和涉及的数字。有些应用程序最终只会有小的四舍五入误差,而有些应用程序可能会出现巨大误差。没有人可以在不了解更多关于您特定操作序列的信息的情况下为误差公差提供建议。此外,解决方案可能不是接受比较数字的公差,而是重新设计您的计算以避免错误,或者至少减少错误。

没有一般性的解决方案可用于比较“相等”的浮点值,因为任何这样的解决方案都是不可能存在的。


我现在已经编辑了问题。我同意对于具体的应用程序,人们可能知道什么容差是可以接受的,并且应该根据这个定义相等。但我的问题不太具体于应用程序,而更多地关注通过检查数字的底层位表示可以实现什么。 - augustzf
2
@augustzf:在这种情况下,检查位并不能实现任何有用的操作。一般来说,任何有用的测试都可以通过常规浮点运算来完成。如果你只是想知道浮点数是如何工作和应该如何使用,而不是试图解决具体的应用问题,那么答案是浮点数并非为此设计。 - Eric Postpischil
1
是的,我在阅读了关于同一主题的这篇文章后,现在得出了相同的结论。 - augustzf

1
在C++中,我使用一个函数(搜索nearly_equal())在Go中看起来像这样:
func nearlyEqual(a, b, epsilon float64) {

    // already equal?
    if(a == b) {
        return true
    }

    diff := math.Abs(a - b)
    if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 {
        return diff < epsilon * math.SmallestNonzeroFloat64
    }

    return diff / (math.Abs(a) + math.Abs(b)) < epsilon
}

这将计算输入 ab 之间的差异,然后确保差异在给定的 epsilon 范围内。除法运算移除了量纲,使比较变得简单。


问题在于浮点数使用其尾数中定义的位数。尾数所代表的内容取决于指数。因此,如果您只是比较位,您首先需要“适当地移位”。上面的操作在最后一行完成了这个过程。除法等同于移位。

假设我们有非常大的整数,我们可以使用大约700位的定点数,其中小数点左侧有大约350位,小数点右侧有另外350位:

700             350               0
XXX ... XXXXXXX '.' XXXXXXX ... XXX

float64的尾数有56位,因此在上述数字表示中,除了那56位之外,大多数位都将是零。如果一个数字在最左边有这56位,而另一个数字在最右边也有56位,那么这两个数字非常不同,上述代码可以检测到这一点。

使用这些定点数,我们可以做更多或更少像这样的事情:

// assume "fp" numbers are fixed point numbers and the shift works on those
fp_a := a << (350 + a.exponent)
fp_b := b << (350 + b.exponent)

fp_diff := math.Abs(fp_a - fp_b)

return fp_diff < fp_epsilon

问题在于实现700位数值是不切实际的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接