Go float比较

ssm49v7z  于 2023-06-03  发布在  Go
关注(0)|答案(3)|浏览(467)

为了在Go中比较两个float(float64)是否相等,我对IEEE 754和float的二进制表示的肤浅理解使我认为这是一个很好的解决方案:

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更通用、更精确、更有效的方法来比较两个任意大小的浮点数是否“几乎相等”?我的理由是,如果在二进制表示中只允许一位的差异,那么比较的数字肯定不可能更相等,除了严格相等之外,显然(如注解中所指出的)可以用==检查浮点数。
注:我已经编辑了这个问题,使它更清楚。

e5nszbig

e5nszbig1#

不要使用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))
}
5vf7fwbs

5vf7fwbs2#

不,这不是比较浮点值的正确方法。
您实际上并没有说明真实的的问题-您试图比较两个浮点数是有原因的,但您没有说明它是什么。
浮点运算被设计为执行近似运算。在浮点运算中会有舍入误差的累积是正常的。当以不同的方式计算值时,这些错误通常是不同的,因此不应期望浮点运算产生相同的结果。
在您的示例中,发生了以下操作:

  • 十进制数字“0.1”被转换为float64(IEEE-754 64位二进制浮点)。这产生了值0.100000000000000055511151231257827021181583404541015625,这是最接近0.1的float64值。
  • 十进制数字“0.2”转换为float64。这产生了0.200000000000000011102230246251565404236316680908203125,这是最接近0.2的float64值。
  • 添加了这些内容。这产生了0.3000000000000000444089209850062616169452667236328125。除了当0.1和0.2四舍五入到float64中的最接近值时发生的舍入误差外,这还包含一些额外的舍入误差,因为精确的总和无法在float64中表示。
  • 十进制数字“0.3”转换为float64。这产生了0.299999999999999998897769753748434595763683319091796875,这是最接近0.3的float64值。

正如您所看到的,0.10.2相加的结果累积了与0.3不同的舍入误差,因此它们不相等。没有正确的相等测试会报告它们相等。而且,重要的是,本例中出现的错误是本例特有的--不同的浮点运算序列会有不同的错误,累计的错误并不局限于数字的低位。
有些人试图通过测试差异是否小于某个小值来进行比较。这在某些应用程序中是可以的,但在您的应用程序中也可以吗?我们不知道你在做什么,所以我们不知道会发生什么问题。允许小误差的测试有时会报告不正确的结果,要么是假阳性(因为它们接受如果用精确数学计算则不相等的相等数字),要么是假阴性(因为它们拒绝如果用精确数学计算则相等的数字的相等)。对于您的应用程序来说,这些错误中哪一个更糟糕?其中一个会导致机器损坏或人员受伤吗?如果不知道这一点,没有人可以建议哪一个错误的结果是可以接受的,或者即使是。
此外,误差的容限应该有多大?计算中可能出现的总误差取决于所执行的操作的顺序和所涉及的数字。一些应用程序将仅具有小的最终舍入误差,并且一些应用程序可能具有大量误差。如果不了解您的具体操作顺序,没有人可以给予关于公差使用什么值的建议。此外,解决方案可能不是在比较数字时接受公差,而是重新设计计算以避免错误,或者至少减少错误。
不存在比较浮点值是否“相等”的通用解决方案,因为不可能存在任何此类解决方案。

xvw2m8pv

xvw2m8pv3#

在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位的数字是不实际的。

相关问题