Skip to content

Latest commit

 

History

History
128 lines (89 loc) · 6 KB

3-3-not-understanding-floating-points.md

File metadata and controls

128 lines (89 loc) · 6 KB

3.3 不被理解的浮点型

在 Go 中,有两种浮点类型(如果我们省略虚数):float32float64。发明浮点的概念是为了解决整数的主要问题:它们无法表示小数值。为了避免意外,我们必须知道浮点算术是实数算术的近似值。让我们发现使用近似值的影响以及如何提高准确性。

让我们看一个乘法示例:

var n float32 = 1.0001
fmt.Println(n * n)

我们可能希望这段代码打印 1.0001 * 1.0001: 1.00020001 的结果,对吧?但是,在大多数 x86 处理器上运行此代码会打印 1.0002。我们该如何解释呢?让我们了解浮点数的算法。

如果我们以 float64 类型为例,我们应该注意到在 math.SmallestNonzeroFloat64float64 最小值)和 math MaxFloat64float64 最大值)之间存在无限多个实数值。相反,float64 类型的位数是有限的:64。由于不可能使无限值适合有限空间,因此我们必须使用近似值。 因此,可能会失去精度。float32 类型也是如此。

Go 中的浮点数遵循 IEEE-754 标准,其中一些位表示尾数,其他位表示指数。尾数是基值,而指数是应用于尾数的乘数。在单精度浮点数(float32)中,8 位表示指数,23 位表示尾数。在双精度浮点 (float64) 中,指数和尾数分别为 11 位和 52 位。剩下的位是符号。要将浮点数转换为小数,我们进行以下计算:

sign * 2^exponent * mantissa

让我们看看1.0001作为 float32 的表示:

指数使用 8 位过量/偏置表示法。在这里,01111111 的指数值表示 2^0,而尾数等于 1.000100016593933(本节的范围不是解释转换的工作原理)。因此,十进制值等于 1 * 2^0 * 1.000100016593933。因此,我们存储到单精度浮点中的不是 1.0001,而是 1.000100016593933。缺乏精度会影响存储值的准确性。

一旦我们了解到 float32float64 是近似值,开发人员对我们有什么影响?

第一个含义与比较有关。 使用 == 运算符比较两个浮点数可能会导致不准确。相反,我们应该比较它们的差异小于某个小的误差值。例如,testify 测试库有一个 InDelta 函数来断言两个值在彼此的给定增量内。

我们还要记住,浮点计算的结果取决于实际的处理器。大多数处理器都有一个浮点单元 (FPU) 来处理此类计算。不能保证在一台机器上执行的一个结果在另一台具有不同 FPU 的机器上是相同的。同样,使用 delta 比较两个值可以成为跨不同机器实现有效测试的解决方案。

Note 这里有一个构建这些特殊类型数字的例子:

var a float64
positiveInf := 1 / a
negativeInf := -1 / a
nan := a / a
fmt.Println(positiveInf, negativeInf, nan)

这段代码输出如下:

+Inf -Inf NaN

我们可以使用 math.IsInf 检查浮点数是否无限,以及使用 math.IsNaN 检查浮点数是否为 NaN。

Go 还有三种特殊的浮点数:

  • 正无限。
  • 负无限。
  • NaN(不是一个数字),未定义或无法表示的操作的结果。 根据 IEEE-754,唯一满足 f != f 的浮点数。

我们已经看到十进制到浮点的转换会导致精度下降。这是由于转换导致的错误。我们还必须注意,错误可以在一系列浮点运算中累积。

让我们看一个示例,其中有两个函数将以不同的顺序执行相同的操作序列。f1 将首先将 float64 初始化为 10000,然后将 1.0001 重复添加到此结果(n 次)。相反,f2 将执行相同的操作,但顺序相反(最后加 10000)。

func f1(n int) float64{
    result := 10_000.
    for i := 0; i < n; i++ {
        result += 1.0001
    }
    return result
}

func f2(n int) float64 {
    result := 0.
    for i := 0; i < n; i++ {
        result += 1.0001
    }
    return result + 10_000.
}

让我们在x86处理器上运行这些功能,同时改变n:

n 期望结果 f1 f2
10 10010.01 10010.000999999993 10010.001
1k 11000.1 11000.099999999293 11000.099999999982
1m 1.0101e+06 1.0100999999761417e+06 1.0100999999766762e+06

我们可以注意到,n 越大,不精确就越大。然而,我们也可以看到 f2 精度优于f1。事实上,我们应该记住,浮点计算的顺序可能会影响结果的准确性。

在执行一系列加法和减法时,我们应该将操作分组以添加或减去具有相似数量级的值,然后再添加或减去那些幅度不接近的值。由于 f2 执行了 10000 加法,因此最终产生的结果比 f1 更准确。

乘法和除法呢?让我们想象一下,我们想计算以下内容:

a * (b + c)

如我们所知,这种计算等于:

a * b + a * c

让我们用与 bc 不同数量级的 a 运行这两个计算:

a := 100000.001
b := 1.0001
c := 1.0002

fmt.Println(a * (b + c))
fmt.Println(a*b + a*c)
200030.00200030004
200030.0020003

确切的结果是 200030.002。因此,第一次计算的准确度最差。 确实,在进行加减乘除的浮点运算时,我们必须先完成乘除运算才能获得更好的精度。有时,它可能会影响执行时间,就像前面的示例一样,它需要三个操作而不是两个。在这种情况下,这是准确性和执行时间之间的选择。

Go的 float32float64 是近似值。因此,我们必须牢记一些规则:

  • 在比较两个浮点数时,我们应该在可接受的范围内比较它们的差异。

  • 我们应该对操作进行类似数量级的分组,以便在执行加法或减法时提高准确性。

  • 同样,为了提高准确性,如果一系列操作需要加法、减法、乘法和除法,我们应该先执行乘法和除法。

以下部分将开始深入研究切片,并讨论两个关键概念:切片的长度和容量。