floating-point - 为什么 float 不正确?

标签 floating-point language-agnostic precision

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

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

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


这样一个看似简单的数字怎么会“太大”而无法在64位内存中表达呢?

最佳答案

在大多数编程语言中,浮点数非常类似于scientific notation表示:具有指数和尾数(也称为有效位数)。一个非常简单的数字,例如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。回想一下,科学计数法中的尾数始终以单个非零数字开头。二进制也是如此,只不过二进制只有两位数字:01。因此二进制尾数始终以1开头!当存储浮点数时,将省略二进制尾数前面的1以便节省空间。我们必须将其放回第三个元素的前面以获取真实的尾数:


  1.0010011001100110011001100110011001100110011001100110110


这涉及的不仅仅是一个简单的加法,因为存储在我们第三个分量中的位实际上代表了radix point右边的尾数的小数部分。

在处理十进制数时,我们通过乘以10的乘方或除以“移动小数点”。在二进制中,我们可以通过乘以2的乘方或除以进行相同的操作。由于我们的第三个元素有52位,因此我们除以通过252将其向右移动52个位置:


  0.00100110011001100110011001100110011001100110011001100110


用十进制表示法,与将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 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






进一步阅读


The Floating-Point Guide: What Every Programmer Should Know About Floating-Point Arithmetic, or, Why don’t my numbers add up?(floating-point-gui.de)
What Every Computer Scientist Should Know About Floating-Point Arithmetic(Goldberg 1991)
IEEE Double-precision floating-point format(维基百科)
Floating Point Arithmetic: Issues and Limitations(docs.python.org)
Floating Point Binary

关于floating-point - 为什么 float 不正确?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27319176/

相关文章:

java - 如何使用 Apache POI 设置漂亮的浮点值?

language-agnostic - 在给定距离的线上寻找点

arrays - 具有显式 double 的 Fortran 数组

language-agnostic - 哪些层应该使用领域模型?

language-agnostic - 出货前重构还是出货后重构?

iphone - map 套件中的 GPS 精度不如 Maps.app 中那么精确

math - float 学有问题吗?

floating-point - FMA : proof performance

floating-point - 一般编程 : Decimal numbers, float

c++ - 为什么pow(x,1/p)和pow(x,1.0/p)不相等,即使打印它们的值会得到相同的结果