为什么有些数字存储为浮点数时会失去准确性?
例如,十进制数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
。回想一下,科学计数法中的尾数始终以单个非零数字开头。二进制也是如此,只不过二进制只有两位数字:0
和1
。因此二进制尾数始终以1
开头!当存储浮点数时,将省略二进制尾数前面的1
以节省空间。我们必须将其放回第三个元素的前面以获取真实的尾数:1.0010011001100110011001100110011001100110011001100110110
这涉及的不仅仅是一个简单的加法,因为存储在我们第三个分量中的位实际上代表了radix point右边的尾数的小数部分。
在处理十进制数字时,我们通过乘以10的乘方或除以“移动小数点”。在二进制中,通过乘以2的乘方或除以可以做相同的事情。由于我们的第三个元素有52位,因此我们除以通过252将其向右移动52个位置:
0.0010011001100110011001100110011001100110011001100110110
用十进制表示法,与将
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