为什么浮点数不准确?

259

为什么有些数字以浮点数存储时会失去精度?

例如,十进制数 9.2 可以完全表示为两个十进制整数的比率(92/10),这两个整数都可以用二进制表示并且能够完全表示(0b1011100/0b1010)。然而,将相同的比率存储为浮点数时,它永远不会完全等于 9.2

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

一个看似简单的数字怎么可能“太大”以至于在64位内存中无法表示呢?


7
在Meta上讨论此帖子(Discussion of this post on Meta):http://meta.stackoverflow.com/questions/260130/canonical-duplicate-for-floating-point-is-inaccurate。 - Shog9
6
参考is floating math broken - LF00
1
9.2并不是在64位内存中表达“太大”,它只是简单地不等于二进制64值允许的1022 * 2 ^ 52 = 4602678819172646912个预定义值之一,因此它会被四舍五入到最接近的一个。 - RBF06
5个回答

318

在大多数编程语言中,浮点数的表示方式与科学计数法非常相似:它包含一个指数和一个尾数(也称为有效数字)。一个非常简单的数,比如9.2,实际上可以表示为以下分数:

5179139571476070 * 2 -49

其中指数为-49,尾数为5179139571476070。无法用这种方式表示某些十进制数的原因是指数和尾数都必须为整数。换句话说,所有浮点数都必须是一个整数乘以2的整数次幂

9.2可能只是92/10,但如果将n限制为整数值,10就不能表示为2n


查看数据

首先,有几个函数可以“查看”组成32位和64位float的组件。如果您只关心输出结果,请忽略这些(示例使用Python):

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)]

那个函数背后有很多复杂性,解释起来会很离题,但如果你感兴趣的话,对我们来说重要的资源是struct模块。

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

当我们使用示例9.2调用该函数时,我们得到以下结果:

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

解读数据

你会看到我将返回值分成了三个组成部分。这些组成部分包括:

  • 符号
  • 指数
  • 尾数(也称为有效数字或小数部分)

符号

符号以单个位存储在第一个组成部分中。很容易解释:0表示浮点数是正数;1表示浮点数是负数。因为9.2是正数,所以我们的符号值是0

指数

指数以11位的形式存储在中间组件中。在我们的例子中,是0b10000000010。换算成十进制,表示的值是1026。这个组件的一个特殊之处是,你必须减去一个等于2(位数) - 1 - 1的数字才能得到真正的指数;在我们的例子中,意味着要减去0b1111111111(十进制数1023),得到真正的指数0b00000000011(十进制数3)。

尾数

尾数以52位的形式存储在第三个组件中。然而,这个组件也有一个特殊之处。为了理解这个特殊之处,考虑科学计数法中的一个数字,如下所示:

6.0221413x1023

尾数将是6.0221413。请记住,科学计数法中的尾数始终以一个非零数字开头。对于二进制来说也是如此,只不过二进制只有两个数字:0和1。因此,二进制尾数总是以1开头!当存储浮点数时,二进制尾数前面的1被省略以节省空间;我们必须将其放回到第三个元素的开头,以获得真正的尾数。

1.0010011001100110011001100110011001100110011001100110

这不仅仅是简单的加法,因为我们第三个组件中存储的位实际上代表了尾数的小数部分,在小数点的右侧。
在处理十进制数时,我们通过乘以或除以10的幂来“移动小数点”。在二进制中,我们可以通过乘以或除以2的幂来做同样的事情。由于我们的第三个元素有52位,我们将它除以252来将其向右移动52位:

0.0010011001100110011001100110011001100110011001100110

在十进制表示法中,这相当于将675539944105574除以4503599627370496得到0.1499999999999999。(这是一个比率的例子,在十进制中可以准确表示,但在二进制中只能近似表示;有关更多详细信息,请参见:675539944105574 / 4503599627370496。)
现在我们已经将第三个组件转换为分数,加上1就得到了真正的尾数。
总结一下各个组成部分:
- 符号(第一个组件):正数为0,负数为1 - 指数(中间组件):减去2的(位数-1)次方-1,得到真正的指数 - 尾数(最后一个组件):除以2的(位数)次方,并加上1,得到真正的尾数

计算数字

将这三部分放在一起,我们得到以下二进制数:

1.0010011001100110011001100110011001100110011001100110 x 1011

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

1.1499999999999999 x 23(不精确!)

通过乘法运算,我们可以得到最终表示我们开始时的数字(9.2),该数字被存储为浮点值。

9.1999999999999993


以分数形式表示

9.2

现在我们已经构建了这个数字,可以将其重构为一个简单的分数:
1.0010011001100110011001100110011001100110011001100110 x 10^11
将尾数移动到整数部分:
10010011001100110011001100110011001100110011001100110 x 10^(11-110100)
转换为十进制:
5179139571476070 x 2^(3-52)
减去指数:
5179139571476070 x 2^-49
将负指数转换为除法:
5179139571476070 / 2^49
乘以指数:
5179139571476070 / 562949953421312
等于:

9.1999999999999993

9.5

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

已经可以看到尾数只有4位数字,后面跟着一大串的零。但是让我们按部就班地进行下去。
组装二进制科学计数法:
1.0011 x 10^11
移动小数点:
10011 x 10^(11-100)
减去指数:
10011 x 10^-1
二进制转十进制:
19 x 2^-1
负指数转为除法:
19 / 2^1
乘以指数:
19 / 2
等于:

9.5



进一步阅读


2
还有一个不错的教程,展示了如何反向操作 - 给定一个十进制数的表示,如何构造浮点数等效物。 "长除法"方法非常清楚地展示了在尝试表示数字后如何得到“余数”。如果您想要真正“规范化”您的答案,应该添加这个教程。 - Floris
1
如果你在谈论Python和浮点数,我建议至少在你的链接中包含Python教程:http://docs.python.org/3.4/tutorial/floatingpoint.html 这应该是Python程序员解决浮点问题的一站式资源。如果它在某些方面缺乏(几乎肯定是),请在Python错误跟踪器上打开一个问题以获取更新或更改。 - Mark Dickinson
如果这被转换为社区维基,请随意将我的答案合并到您的答案中。 - Nicu Stiurca
7
这个回答应该一定要链接到floating-point-gui.de,因为这可能是初学者最好的介绍。在我看来,它甚至应该放在“每个计算机科学家都应该知道……”之上——现在,那些能够合理理解Goldberg的论文的人通常已经很清楚了。 - Daniel Pryden
3
这是一个可以在二进制中精确表示,但在十进制中只能近似表示的比率示例。这并不正确。所有这些“数字除以2的幂”的比率在十进制中都是精确的。任何近似值仅用于缩短小数 -- 为了方便起见。 - Rick Regan
显示剩余2条评论

46

这不是一个完整的答案(mhlester已经涵盖了很多我不会重复的好内容),但我想强调数的表示在很大程度上取决于你所使用的进制。

考虑分数 2/3

在基数为10的情况下,我们通常将其写成以下形式:

  • 0.666...
  • 0.666
  • 0.667

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

数的进制

所以在这里进制非常关键。如果我们试图在基数为3的情况下表示2/3,则

(2/3)10 = 0.23

换句话说,通过转换进制,我们可以得到相同数的精确有限表示!结论是,尽管你可以将任何数字转换为任何进制,所有有理数在某些基数下具有精确的有限表示,但在其他基数下则不然。

为了更好地说明这一点,让我们来看看1/2。这个完全简单的数字可能会让你惊讶,即使这个数字在10进制和2进制中都有精确表示,但在3进制中需要一个重复的表示。

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

为什么浮点数不准确?

因为它们通常是用二进制近似表示无法有限表示的有理数(数字重复),并且一般来说,它们是近似表示实数(可能是无理数),在任何进制下都不能用有限位数表示。


9
换句话说,“三进制”对于“1/3”就像“十进制”对于“1/10”一样完美。无论是哪个分数在“二进制”中都不起作用。 - mhlester
3
是的。通常来说,N进制适用于任何分母为N或其倍数的分数。 - Nicu Stiurca
5
这也是一些数值工具箱跟踪“除以什么”的原因之一,在此过程中可以对所有有理数保持“无限精度”。就像物理学家喜欢将他们的方程式保留为符号形式,直到可能的最后一刻,以防万一 π 等因素被消除。 - Floris
3
我也看到过这样的情况,其中一个只执行基本算术(即保留输入的有理性)的算法会确定输入是否(可能)合理,使用正常的浮点算术执行数学运算,然后在最后重新估计一个有理近似值以修复任何舍入误差。特别是Matlab的行简化阶梯形式算法就是这样做的,这有助于提高数值稳定性。 - Nicu Stiurca
@SchighSchagh - 很有趣,我不知道这一点。但我确实知道,在这个双倍精度的时代,数值稳定性并没有得到足够的教授。这意味着许多人错过了学习很多美妙算法的机会。我真的很喜欢能够计算和纠正自己错误的算法。 - Floris
显示剩余2条评论

17

虽然其他答案都不错,但还有一点遗漏:

无法精确表示无理数(例如π、sqrt(2)log(3)等)!

这也是它们被称为无理数的原因。世界上无论存储比特数量多少,都无法精确表示它们。只有符号算术才能保持它们的精度。

尽管如果你的数学需求仅限于有理数,那么精度问题就可以得到解决。你需要存储一对(可能非常大的)整数ab来表示由分数a/b表示的数字。所有的算术运算都必须基于分数,就像在高中数学中一样(例如,a/b * c/d = ac/bd)。

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

简而言之

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


5
有趣的是,存在着不合理的进位制。例如,黄金比例进位制 - Veedrac
6
无理数只能用它们所在的进位制表示。例如,圆周率在以圆周率为底的进位制下表示为10。 - phuclv
6
核心观点依然有效:无论使用何种进制,有些数字永远无法表示。改变进制并不能获得任何好处,因为这样会导致其他一些数字也无法被表示。 - Jonas Bötel
1
所有可构造的实数都可以在适当的基础下被精确地表示出来;对于任何特定的数字,选择基础实际上是无限的。例如,pi在基础为pi时是10,在基础为sqrt(pi)时是100。一般来说,x在基础为x时是10,在基础为x^(1/2)时是100,在基础为x^(1/3)时是1000,等等。如果您通过公理的选择允许它们存在,则不可构造的实数会变得非常奇怪,无论如何也没有人关心数字。尽管如此,这些神秘的基础并不真正有用;而且无论您选择什么样的基础,总会有无理数存在。 - Nicu Stiurca

9
有无限多的实数(数量太多以至于不能枚举),也有无限多的有理数(可以枚举)。浮点数表示是有限的(像计算机中的任何东西一样),所以许多许多很多的数字无法表示。特别地,64位只允许您区分仅18,446,744,073,709,551,616个不同的值(与无限相比微不足道)。按照标准惯例,9.2不是其中之一。那些可以用m.2^e的形式表示为某些整数m和e。
您可能会想出一个不同的数字系统,例如基于10的系统,在这个系统中,9.2将具有精确的表示。但是其他数字,比如1/3,仍然无法表示。
此外,请注意双精度浮点数非常准确。它们可以表示非常广范围内的任何数字,并且具有多达15位的精确数字。对于日常生活计算,4或5个数字已经足够了。除非您想计算您一生中的每毫秒,否则您永远不需要这15个数字。

2
为什么我们无法用二进制浮点数表示9.2?
浮点数(稍微简化一下)是一个带有受限位数和可移动基数点的位置计数系统。
如果分数在最简形式下的分母的质因数是基数的因子,则只能使用有限数量的数字在位置计数系统中精确地表示分数。
数字10的质因数为5和2,因此在十进制中,我们可以表示任何形如a/(2^b*5^c)的分数。
另一方面,数字2的唯一质因数是2,因此在二进制中,我们只能表示形如a/(2^b)的分数。
为什么计算机使用这种表示法?
因为它是一种易于处理且足够准确的格式。基本上,科学家使用“科学计数法”,并在每个步骤将结果舍入到合理数量的数字,原因基本相同。
当然,定义一种分数格式是可能的,例如32位分子和32位分母。它能够表示IEEE双精度浮点数不能表示的数字,但同样地,有许多数字可以用双精度浮点数来表示,而不能用这种固定大小的分数格式来表示。
然而,大问题是这种格式很难进行计算。由于两个原因。
1. 如果要每个数字都有唯一的表示,则在每个计算后,您需要将分数约简为最简形式。这意味着对于每个操作,您基本上需要进行最大公约数计算。 2. 如果您的计算结果无法表示,则需要找到最接近可表示结果的结果。这是非常不容易的。
某些语言确实提供了分数类型,但通常它们与任意精度相结合,这避免了需要担心近似分数,但会产生自己的问题,当数字通过大量计算步骤时,分母的大小和因此所需的存储空间可能会爆炸。
一些语言还提供十进制浮点类型,这些类型主要用于场景中,其中计算机得到的结果必须与人类考虑的预先存在的舍入规则匹配(主要是财务计算)。这些比二进制浮点更难处理,但最大的问题是大多数计算机没有为其提供硬件支持。

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