为什么浮点数不准确?

Why are floating point numbers inaccurate?

提问人: 提问时间:2/20/2014 最后编辑:9 revs, 6 users 68%mhlester 更新时间:6/27/2023 访问量:80296

问:

为什么有些数字在存储为浮点数时会失去准确性?

例如,十进制数可以精确地表示为两个十进制整数()的比率,两者都可以精确地用二进制()表示。但是,存储为浮点数的相同比率永远不会完全等于:9.292/100b1011100/0b10109.2

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

这么一个简单的数字怎么会“太大”而无法用 64 位内存来表达呢?

浮点 与语言无关的 精度

评论

7赞 Shog9 6/13/2014
在 Meta 上讨论这篇文章
6赞 LF00 5/23/2017
参考浮动数学是否损坏
1赞 RBF06 9/14/2022
9.2在 64 位内存中表示并不是“太大”,它只是不完全等于 binary64 值允许的 1022*2^52 = 4602678819172646912 个预定义值中的任何一个,因此它被舍入到最接近的值。

答:

320赞 8 revs, 5 users 90%mhlester #1

在大多数编程语言中,浮点数的表示方式很像科学记数法:用指数和尾数(也称为有效数)表示。一个非常简单的数字,比如说,实际上是这个分数:9.2

5179139571476070 * 2 -49

其中指数是,尾数是 。不可能以这种方式表示某些十进制数的原因是指数和尾数都必须是整数。换句话说,所有浮点数都必须是整数乘以 2 的整数幂-495179139571476070

9.2可以简单地表示 ,但如果 n 限制为整数值,则 10 不能表示为 2n92/10


查看数据

首先,通过几个函数来查看构成 32 位和 64 位的组件。如果你只关心输出(Python 中的示例),请忽略这些内容:float

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

这个函数背后有很多复杂性,解释起来会很麻烦,但如果你有兴趣,我们目的的重要资源是结构模块。

Python 是一个 64 位的双精度数字。在其他语言如C,C++,Java和C#中,双精度有一个单独的类型,通常实现为64位。floatdouble

当我们用我们的示例调用该函数时,我们得到的是:9.2

>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

解释数据

您将看到我已将返回值拆分为三个组件。这些组件包括:

  • 标志
  • 指数
  • 尾数(也称为 Significand 或分数)

标志

符号作为单个位存储在第一个组件中。这很容易解释:表示浮点数是正数; 意味着它是负面的。因为是正数,所以我们的符号值是 。019.20

指数

指数以 11 位的形式存储在中间分量中。在我们的例子中,.在十进制中,表示值 。这个组件的一个怪癖是,你必须减去一个等于 2(# 位) - 1 - 1 的数字才能得到真正的指数;在我们的例子中,这意味着减去(十进制数)以获得真正的指数(十进制数 3)。0b1000000001010260b111111111110230b00000000011

尾数

尾数以 52 位的形式存储在第三个组件中。但是,这个组件也有一个怪癖。要理解这个怪癖,请考虑科学记数法中的数字,如下所示:

6.0221413x1023

尾数将是.回想一下,科学记数法中的尾数总是以一个非零数字开头。二进制也是如此,只是二进制只有两位数:和 .所以二进制尾数总是以 !当存储浮点数时,省略二进制尾数前面的尾数以节省空间;我们必须将它放回第三个元素的前面才能得到真正的尾数:6.02214130111

1.0010011001100110011001100110011001100110011001100110

这不仅仅是一个简单的加法,因为存储在第三个组件中的位实际上代表了尾数的小部分,位于基点的右侧。

在处理十进制数时,我们通过乘以或除以 10 的幂来“移动小数点”。在二进制中,我们可以通过乘以或除以 2 的幂来做同样的事情。由于我们的第三个元素有 52 位,我们将其除以 2 52 以将其向右移动 52 位:

0.0010011001100110011001100110011001100110011001100110

在十进制表示法中,这与除以得到 .(这是一个比率的例子,可以精确地用十进制表示,但只能近似地用二进制表示;有关详细信息,请参阅:675539944105574 / 450359962737049667553994410557445035996273704960.1499999999999999

现在我们已经将第三个分量转换为小数,加法得到真正的尾数。1

回顾组件

  • 符号(第一部分):表示正数,表示负数01
  • 指数(中间分量):减去 2(# 位) - 1 - 1 得到真正的指数
  • 尾数(最后一个部分):除以 2(# 位)并相加得到真正的尾数1

计算数字

将所有三个部分放在一起,我们得到这个二进制数:

1.0010011001100110011001100110011001100110011001100110 x 1011

然后我们可以将其从二进制转换为十进制:

1.14999999999999999 x 23 (不准确!

并相乘以显示我们以 () 开头的数字在存储为浮点值后的最终表示:9.2

9.1999999999999993


表示为分数

9.2

现在我们已经构建了这个数字,可以将其重建为一个简单的分数:

1.0010011001100110011001100110011001100110011001100110 x 1011

将尾数转换为整数:

10010011001100110011001100110011001100110011001100110 x 1011-110100

转换为十进制:

5179139571476070 x 23-52

减去指数:

5179139571476070 x 2-49

将负指数转换为除法:

5179139571476070 / 249

乘法指数:

5179139571476070 / 562949953421312

这等于:

9.1999999999999993

9.5

>>> float_to_bin_parts(9.5)
['0', '10000000010', '0011000000000000000000000000000000000000000000000000']

你已经可以看到尾数只有 4 位数字,后面跟着一大堆零。但是,让我们来看看这些步骤。

组合二元科学记数法:

1.0011 x 1011

移动小数点:

10011 x 1011-100

减去指数:

10011 x 10-1

二进制到十进制:

约19 x 2-1

除法的负指数:

19 / 21

乘法指数:

19 / 2

等于:

9.5



延伸阅读

评论

2赞 Floris 2/20/2014
还有一个很好的教程,展示了如何走另一条路 - 给定一个数字的十进制表示,你如何构造浮点等价物。“长除法”方法非常清楚地显示了在尝试表示数字后如何最终得到“余数”。如果你想让你的答案真正“规范”,应该添加。
1赞 Mark Dickinson 2/20/2014
如果你在谈论 Python 和浮点,我建议至少在你的链接中包含 Python 教程: docs.python.org/3.4/tutorial/floatingpoint.html 这应该是 Python 程序员解决浮点问题的一站式资源。如果它在某种程度上有所欠缺(几乎可以肯定是),请在 Python 错误跟踪器上打开一个问题以获取更新或更改。
0赞 Nicu Stiurca 2/21/2014
@mhlester 如果它变成了社区维基,请随时将我的答案合并到你的答案中。
7赞 Daniel Pryden 6/14/2014
这个答案肯定也应该链接到 floating-point-gui.de,因为它可能是初学者最好的介绍。IMO,它甚至应该超越“每个计算机科学家都应该知道的事情......”——如今,能够合理理解戈德堡论文的人通常已经很清楚了。
3赞 Rick Regan 12/22/2015
“这是一个比率的例子,可以精确地用二进制表示,但只能近似地用十进制来表示”。事实并非如此。所有这些“数字超过二的幂”比率都是精确的十进制。任何近似值都只是为了缩短十进制数——为了方便起见。
47赞 4 revs, 2 users 98%Nicu Stiurca #2

这不是一个完整的答案(mhlester 已经涵盖了很多我不会重复的好地方),但我想强调一个数字的表示在多大程度上取决于你正在工作的基础。

考虑分数 2/3

在 good-ol' base 10 中,我们通常将其写成类似

  • 0.666...
  • 0.666
  • 0.667

当我们查看这些表示时,我们倾向于将它们中的每一个都与分数 2/3 相关联,即使只有第一个表示在数学上等于分数。第二和第三种表示/近似值的误差约为 0.001,这实际上比 9.2 和 9.1999999999999993 之间的误差要差得多。事实上,第二个表示甚至没有正确舍入!尽管如此,我们对 0.666 作为数字 2/3 的近似值没有问题,因此我们对 9.2 在大多数程序中的近似方式应该没有问题(是的,在某些程序中,这很重要。

数基数

因此,这就是数字基数至关重要的地方。如果我们试图以 2/3 为基数表示 3,那么

(2/3)10 = 0.23

换句话说,通过切换基数,我们得到了相同数字的精确、有限的表示!结论是,即使您可以将任何数转换为任何基数,所有有理数在某些基数中都具有精确的有限表示,但在其他基数中则没有

为了说明这一点,让我们看看 1/2。您可能会感到惊讶,尽管这个非常简单的数字在基数 10 和 2 中具有精确的表示,但它需要在基数 3 中重复表示。

(1/2)10 = 0.5 10 = 0.12 =0.1111...3

为什么浮点数不准确?

因为很多时候,它们是近似于不能用以 2 为基数(数字重复)有限表示的有理数,而且通常它们是近似于实数(可能是无理数)数,而这些数可能无法在任何基数中以有限数量的数字表示。

评论

10赞 mhlester 2/20/2014
因此,换句话说,base-3 非常适合,就像 base-10 非常适合 一样。两个分数都不适用于以 2 为底1/31/10
3赞 Nicu Stiurca 2/20/2014
@mhlester 是的。一般来说,以 N 为底的任何分数都是完美的,其分母是或其倍数。N
5赞 Floris 2/20/2014
这就是为什么一些数字工具箱会跟踪“什么被什么除以什么”,并在此过程中可以保持所有有理数的“无限准确性”的原因之一。就像物理学家喜欢将他们的方程式保持符号化直到最后一刻一样,以防等因素抵消。π
3赞 Nicu Stiurca 2/20/2014
@Floris我也见过这样的情况:一种算法只执行基本的算术运算(即,保留输入的合理性),确定输入是否(可能)是合理的,使用正常的浮点运算执行数学运算,然后在最后重新估计一个有理近似值以修复任何舍入误差。特别是Matlab的约简行梯形算法可以做到这一点,它极大地帮助了数值稳定性。
0赞 Floris 2/20/2014
@SchighSchagh - 有趣的是,我不知道。我确实知道,在双精度的今天,数值稳定性是没有得到充分教授的东西。这意味着许多人错过了学习许多美丽算法的优雅。我真的很喜欢计算和纠正自己错误的算法。
17赞 Jonas Bötel #3

虽然所有其他答案都很好,但仍然缺少一件事:

精确地表示无理数(例如 π、sqrt(2)log(3) 等)是不可能的!

这实际上就是为什么它们被称为非理性的原因。世界上再多的比特存储也不足以容纳其中之一。只有符号算术才能保持它们的精确性。

尽管如果你将你的数学需求限制在有理数上,那么只有精确度问题变得可控。您需要存储一对(可能非常大)整数并保存由分数表示的数字。你所有的算术都必须在分数上完成,就像在高中数学中一样(例如)。aba/ba/b * c/d = ac/bd

但是,当然,当涉及 、、 、 等时,您仍然会遇到同样的麻烦。pisqrtlogsin

TL;博士

对于硬件加速算术,只能表示有限数量的有理数。每个不可表示的数字都是近似值。无论系统如何,某些数字(即无理数)永远无法表示。

评论

5赞 Veedrac 6/13/2014
有趣的是,非理性的基础确实存在。例如,Phinary
6赞 phuclv 6/24/2014
无理数可以(仅)用它们的基数表示。例如,pi 在基数 pi 中为 10
6赞 Jonas Bötel 6/27/2014
观点仍然有效:无论系统如何,某些数字都永远无法表示。改变基数不会带来任何好处,因为这样就无法再表示其他一些数字了。
1赞 Nicu Stiurca 8/6/2021
所有可构造的实数*都可以精确地表示,给定一个适当的基数;实际上,对于任何特定数字,基数的选择都是无限的。例如,pi 在 base-pi 中为 10,在 base-sqrt(pi) 中为 100。一般来说,x 在 base-x 中是 10,在 base-x^(1/2) 中是 100,在 base-x^(1/3) 中是 1000,依此类推。 *不可构造的实数,如果你通过选择公理来允许它们,呃,是的,狗屎变得非常奇怪,反正没有人再关心数字了。尽管如此,这些深奥的基础并没有真正有用;无论您选择何种基数,总会有无理数。
2赞 plugwash #4

为什么我们不能用二进制浮点数表示 9.2?

浮点数是一种位置编号系统(略微简化),具有有限的位数和可移动的基数。

如果分母的质因数(当分数以最低项表示时)是基数的因数,则分数只能在位置编号系统中使用有限数量的位数精确表示。

10 的质因数是 5 和 2,因此在以 10 为基数的中,我们可以表示形式 a/(2b5c) 的任何部分。

另一方面,2 的唯一质因数是 2,因此在以 2 为基数的情况下,我们只能表示 a/(2b 形式的分数)

为什么计算机使用这种表示?

因为它是一种简单的格式,并且对于大多数用途来说都足够准确。基本上,科学家使用“科学记数法”并在每一步将他们的结果四舍五入到合理的位数的原因相同。

当然可以定义分数格式,例如,使用32位分子和32位分母。它将能够表示IEEE双精度浮点数无法表示的数字,但同样,将有许多数字可以用双精度浮点数表示,而这些数字无法以这种固定大小的分数格式表示。

然而,最大的问题是,这样的格式进行计算是一件痛苦的事情。有两个原因。

  1. 如果你想让每个数字只有一个表示形式,那么在每次计算之后,你需要将分数减少到最低项。这意味着对于每个操作,您基本上都需要进行最大公约数计算。
  2. 如果经过计算,您最终会得到一个无法表示的结果,因为分子或分母您需要找到最接近的可表示结果。这绝非易事。

有些语言确实提供了分数类型,但通常它们会与任意精度相结合,这样就避免了担心近似分数的问题,但它会产生自己的问题,当一个数字经过大量的计算步骤时,分母的大小和分数所需的存储可能会爆炸。

一些语言还提供十进制浮点类型,这些类型主要用于计算机获得的结果与预先存在的舍入规则相匹配的情况,这些规则是为人类编写的(主要是财务计算)。这些比二进制浮点运算稍微难用一些,但最大的问题是大多数计算机不为它们提供硬件支持。

9赞 2 revsuser1196549 #5

有无限多的实数(太多了,你无法枚举它们),还有无限多的有理数(可以枚举它们)。

浮点表示是有限的(就像计算机中的任何东西一样),因此不可避免地无法表示许多许多数字。特别是,64 位只允许您区分 18,446,744,073,709,551,616 个不同的值(与无穷大相比,这算不了什么)。按照标准约定,9.2 不是其中之一。对于某些整数 m 和 e,可以采用 m.2^e 的形式。


你可能会想出一个不同的数字系统,例如基于10,其中9.2将有一个精确的表示。但其他数字,比如 1/3,仍然无法表示。


另请注意,双精度浮点数非常准确。它们可以表示非常宽范围内的任何数字,最多有 15 个精确数字。对于日常生活计算,4 或 5 位数字绰绰有余。你永远不会真正需要这 15 秒,除非你想计算你一生中的每一毫秒。