为什么浮点数不准确?

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

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

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

这样一个明显简单的数字如何“太大”而不能在内存的64位中表达?

69649 次浏览

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

5179139571476070 * 2__abc0

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

9.2可以简单地表示为92/10,但如果n限制为整数值,则10不能表示为2 <一口> n > < /晚餐


查看数据

首先,看到的几个函数是组成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)]

这个函数背后有很多复杂的东西,解释起来很切题,但如果你感兴趣,我们的重要资源是结构体模块。

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(# of bits) - 1 - 1的数字才能得到真正的指数;在我们的例子中,这意味着减去0b1111111111(十进制数1023)来得到真正的指数,0b00000000011(十进制数3)。

尾数

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

6.0221413 x10__abc0

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

1.0010011001100110011001100110011001100110011001100110

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

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

0.0010011001100110011001100110011001100110011001100110

在十进制中,这相当于用675539944105574除以4503599627370496得到0.1499999999999999。(这是一个可以用二进制精确表示,但只能用十进制近似表示的比率的例子;更多细节,参见:675539944105574 / 4503599627370496。)

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

重述组件

  • 符号(第一个组件):0为正,1为负
  • Exponent(中间分量):减去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 x 1011 - 110100

转换为十进制:

5179139571476070 x 23-52

减去指数:

5179139571476070 x 2-49年

将负指数化为除法:

5179139571476070 / 2__abc0

用指数:

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 / 2__abc0

用指数:

19 / 2

等于:

9.5



进一步的阅读

这不是一个完整的答案(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) __abc0 = 0.2__abc1

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

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

(1/2) 10 = 0.5 10 = 0.1 __abc2 3. = 0.1111…

为什么浮点数不准确?

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

虽然所有其他答案都很好,但还有一件事没有解决:

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

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

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

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

博士TL;

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

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

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

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

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

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

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

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

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

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

  1. 如果你想要每个数字只有一种表示,那么在每次计算之后,你需要将分数减少到它的最低项。这意味着对于每个运算你基本上都需要做一个最大公约数的计算。
  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毫秒,除非你想要计算你生命中的每一毫秒。