为什么浮点数不准确?

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

例如,十进制数字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 ,实际上是这个分数:

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

这个函数背后有很多复杂的东西,它可以解释的切线,但是如果你有兴趣,我们的目的的重要资源是结构模块。

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 。 回想一下,科学记数法中的尾数总是以一个非零数字开始。 二进制也是如此,除了二进制只有两个数字: 01 。 所以二进制尾数始终以1开头! 当存储浮点数时,二进制尾数前面的1被省略以节省空间; 我们必须把它放回到第三个元素的前面才能得到真正的尾数:

    1.0010011001100110011001100110011001100110011001100110

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

    在处理小数时,我们通过乘以或除以10的幂来“移动小数点”。在二进制中,我们可以通过乘以或除以2的幂来做同样的事情。因为我们的第三元素具有52位,所以我们将它通过252把它移动到右边的52个地方:

    0.0010011001100110011001100110011001100110011001100110

    用十进制表示法,这与将675539944105574 4503599627370496除以675539944105574得到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 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×1011

    移动小数点:

    10011×1011-100

    减指数:

    10011×10-1

    二进制到十进制:

    19 x 2-1

    除法的负指数:

    19/21

    乘法指数:

    19/2

    等于:

    9.5



    进一步阅读

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

  • 这不是一个完整的答案(mhlester已经涵盖了许多我不会重复的良好基础),但我想强调一个数字的表示取决于您所在的基数。

    考虑2/3分数

    在良好的醇10基础上,我们通常写出类似的东西

  • 0.666 ...
  • 0.666
  • 0.667
  • 当我们查看这些表示时,我们倾向于将它们中的每一个与分数2/3相关联,尽管只有第一表示在数学上等于分数。 第二个和第三个表示/近似值的误差为0.001,实际上比9.2和9.1999999999999993之间的误差要差得多。 事实上,第二种表示甚至不是正确的圆整! 尽管如此,我们并没有将0.666作为2/3的近似值的问题, 所以我们在大多数程序中近似应该不会有问题 。 (是的,在一些方案中很重要。)

    数字基地

    所以这里的数字基地是crutial。 那么,如果我们试图以第三个基数表示2/3

    (2/3)10 = 0.23

    换句话说,我们有一个确切的,有限的表示,通过切换基数来表示相同的数字! 即使您可以将任意数字转换为任何基数,所有有理数在某些基数上都有精确的有限表示,但在其他基数上却没有。

    为了将这一点带回家,我们来看看1/2。 这可能会让你感到惊讶,即使这个非常简单的数字在基数10和2中有精确的表示,它需要在基数3中重复表示。

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

    为什么浮点数不准确?

    因为它们通常是近似的基数2(数字重复)无法有限地表示的合理值,并且通常它们近似于实际(可能不合理)的数字,这些数字可能无法在任何基数的有限数字中表示。


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

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

    这实际上就是为什么他们被称为非理性。 世界上没有多少位存储就足以保存其中的一个。 只有符号算术才能保持其精度。

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

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

    TL; DR

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

    链接地址: http://www.djcxy.com/p/85633.html

    上一篇: Why Are Floating Point Numbers Inaccurate?

    下一篇: Decimal arithmetics in C or C++?