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

例如,十进制数9.2可以精确地表示为两个十进制整数之比(92/10),这两个十进制整数都可以精确地表示为二进制(0b1011100/0b1010)。然而,存储为浮点数的相同比率永远不会完全等于9.2:

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

这么简单的数字怎么可能在64位内存中“太大”了?


当前回答

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

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

在位置编号系统中,如果分母的质因数(当分数用最小值表示时)是底数的因数,则分数只能用有限位数表示。

10的质因数是5和2,所以以10为底,我们可以表示a/(2b5c)这种形式的任何分数。

另一方面,2唯一的质因数是2,所以以2为底我们只能表示a/(2b)这种形式的分数

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

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

当然可以定义一种分数格式,例如,分子是32位,分母是32位。它可以表示IEEE双精度浮点数不能表示的数字,但同样地,也有许多可以用双精度浮点数表示的数字不能用这种固定大小的分数格式表示。

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

如果你想要每个数字只有一种表示,那么在每次计算之后,你需要将分数减少到它的最低项。这意味着对于每个运算你基本上都需要做一个最大公约数的计算。 如果在计算之后你得到了一个不可表示的结果因为分子或分母你需要找到最接近可表示的结果。这不是小事。

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

一些语言还提供小数浮点类型,这些主要用于计算机得到的结果与预先为人类编写的舍入规则(主要是金融计算)匹配非常重要的场景。与二进制浮点数相比,使用这些浮点数稍微困难一些,但最大的问题是大多数计算机不提供对它们的硬件支持。

其他回答

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

5179139571476070 * 2-49

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

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


查看数据

首先,使用几个函数来查看构成32位和64位浮点数的组件。如果你只关心输出,就忽略这些(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的浮点数是一个64位的双精度数。在其他语言中,如C、c++、Java和c#, double-precision有一个单独的类型double,通常实现为64位。

当我们在9.2的例子中调用这个函数时,得到的结果如下:

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

解读数据

您将看到我将返回值分为三个组件。这些组件是:

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

Sign

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

指数

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

尾数

尾数以52位的形式存储在第三个分量中。然而,这个组件也有一个奇怪的地方。为了理解这个怪癖,考虑一个科学计数法中的数字,像这样:

6.0221413 x1023

尾数是6.0221413。回想一下,科学记数法中的尾数总是以单个非零数字开头。这同样适用于二进制,除了二进制只有两个数字:0和1。所以二进制尾数总是以1开头!当存储浮点数时,二进制尾数前面的1被省略以节省空间;我们必须把它放到第三个元素的前面,才能得到真正的尾数:

1.0010011001100110011001100110011001100110011001100110

这不仅仅是一个简单的加法,因为存储在第三个分量中的位实际上表示尾数的小数部分,在基数点的右边。

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

0.0010011001100110011001100110011001100110011001100110

在十进制计数法中,这相当于用675539944105574除以4503599627370496得到0.14999999999999999999。(这是一个可以用二进制精确表示,但只能用十进制近似表示的比率的例子;详细信息请参见:675539944105574 / 4503599627370496。)

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

重述组件

符号(第一个分量):0表示正,1表示负 指数(中间分量):减去2(# of bits) - 1 - 1得到真正的指数 尾数(最后一个分量):除以2(# of bits)再加1得到真正的尾数


计算数字

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

1.0010011001100110011001100110011001100110011001100110 x 1011

然后我们可以把它从二进制转换成十进制:

1.1499999999999999 x 23(不准确!)

然后相乘,以显示我们开始的数字(9.2)被存储为浮点值后的最终表示形式:

9.1999999999999993


用分数表示

9.2

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

1.0010011001100110011001100110011001100110011001100110 x 1011

将尾数移到整数:

10010011001100110011001100110011001100110011001100110 × 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 × 10-1

二进制到十进制:

19 x 2-1

除法的负指数:

19/21

用指数:

19/2

等于:

9.5



进一步的阅读

浮点指南:每个程序员都应该知道的浮点算术,或者,为什么我的数字加不起来?(floating-point-gui.de) 关于浮点运算,每个计算机科学家都应该知道什么(Goldberg 1991) IEEE双精度浮点格式(维基百科) 浮点运算:问题和限制(docs.python.org) 浮点二进制

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

考虑分数2/3

以10为基数,我们通常会写成这样

0.666... 0.666 0.667

When we look at those representations, we tend to associate each of them with the fraction 2/3, even though only the first representation is mathematically equal to the fraction. The second and third representations/approximations have an error on the order of 0.001, which is actually much worse than the error between 9.2 and 9.1999999999999993. In fact, the second representation isn't even rounded correctly! Nevertheless, we don't have a problem with 0.666 as an approximation of the number 2/3, so we shouldn't really have a problem with how 9.2 is approximated in most programs. (Yes, in some programs it matters.)

基地的数量

这就是数字基数至关重要的地方。如果我们想用3为底表示2/3,那么

(2/3)10 = 0.23

换句话说,通过交换基底,我们对同一个数字有了一个精确的、有限的表示!结论是,即使你可以把任何数转换成任何底数,所有有理数在某些底数中都有精确的有限表示,而在其他底数中则没有。

为了说明这一点,我们来看看1/2。你可能会惊讶地发现,尽管这个非常简单的数字以10和2为底有一个精确的表示,但它需要以3为底的重复表示。

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

为什么浮点数不准确?

因为通常情况下,它们是近似的有理数,不能用有限的基数2表示(数字重复),一般情况下,它们是近似的实数(可能是无理数),可能不能用任何基数的有限位数表示。

有无穷多个实数(多到你无法列举),也有无穷多个有理数(可以列举)。

浮点表示法是有限的(就像计算机中的任何东西一样),因此不可避免地,许多许多数字是不可能表示的。特别是,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),对数(3)等)!

这就是为什么它们被称为非理性。世界上再多的位存储也不足以容纳其中的一个。只有符号算术能够保持它们的精确性。

虽然如果你将你的数学需求限制在有理数,只有精度的问题变得易于管理。您需要存储一对(可能非常大的)整数a和b来保存分数a/b所表示的数字。你所有的算术都必须像高中数学一样在分数上完成(例如a/b * c/d = ac/bd)。

当然,当涉及到pi,√,log, sin等时,你仍然会遇到同样的麻烦。

博士TL;

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

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

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

在位置编号系统中,如果分母的质因数(当分数用最小值表示时)是底数的因数,则分数只能用有限位数表示。

10的质因数是5和2,所以以10为底,我们可以表示a/(2b5c)这种形式的任何分数。

另一方面,2唯一的质因数是2,所以以2为底我们只能表示a/(2b)这种形式的分数

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

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

当然可以定义一种分数格式,例如,分子是32位,分母是32位。它可以表示IEEE双精度浮点数不能表示的数字,但同样地,也有许多可以用双精度浮点数表示的数字不能用这种固定大小的分数格式表示。

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

如果你想要每个数字只有一种表示,那么在每次计算之后,你需要将分数减少到它的最低项。这意味着对于每个运算你基本上都需要做一个最大公约数的计算。 如果在计算之后你得到了一个不可表示的结果因为分子或分母你需要找到最接近可表示的结果。这不是小事。

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

一些语言还提供小数浮点类型,这些主要用于计算机得到的结果与预先为人类编写的舍入规则(主要是金融计算)匹配非常重要的场景。与二进制浮点数相比,使用这些浮点数稍微困难一些,但最大的问题是大多数计算机不提供对它们的硬件支持。