为什么有些数字在存储为浮点数时会失去准确性?
例如,十进制数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.0010011001100110011001100110011001100110011001100110
这不仅仅是一个简单的加法,因为存储在我们的第三个组件中的位实际上代表了尾数的小数部分,位于radix point的右侧。
当处理十进制数时,我们通过乘以或除以10的幂来“移动小数点”。在二进制中,我们可以通过乘以或除以2的幂来做同样的事情。由于我们的第三个元素有52位,我们除以它将252移动到右边52个位置:
0.0010011001100110011001100110011001100110011001100110
在十进制表示法中,这与将675539944105574
除以4503599627370496
以获得0.1499999999999999
相同。 (这是比率的一个示例,可以精确地以二进制表示,但仅以十进制表示;有关更多详细信息,请参阅:675539944105574 / 4503599627370496。)
现在我们已经将第三个组件转换为小数,添加1
会给出真正的尾数。
0
为正面,1
为负面1
以得到真正的尾数将所有三个部分放在一起,我们给出了这个二进制数:
1.0010011001100110011001100110011001100110011001100110 x 1011
然后我们可以从二进制转换为十进制:
1.1499999999999999 x 23(不精确!)
并且在存储为浮点值后,乘以显示我们开始的数字(9.2
)的最终表示:
9.1999999999999993
现在我们已经建立了数字,可以将它重建为一个简单的部分:
1.0010011001100110011001100110011001100110011001100110 x 1011
将尾数转换为整数:
10010011001100110011001100110011001100110011001100110 x 1011-110100
转换为十进制:
5179139571476070 x 23-52
减去指数:
5179139571476070 x 2-49
将负指数转换为除法:
5179139571476070 / 249
乘以指数:
5179139571476070 / 562949953421312
等于:
9.1999999999999993
>>> 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
这不是一个完整的答案(mhlester已经涵盖了很多我不会复制的好基础),但我想强调一个数字的表示取决于你工作的基数。
在良好的基础10中,我们通常会将其写成类似的东西
当我们查看这些表示时,我们倾向于将它们中的每一个与分数2/3相关联,即使只有第一个表示在数学上等于分数。第二和第三表示/近似的误差大约为0.001,实际上比9.2和9.1999999999999993之间的误差差。事实上,第二个表示甚至没有正确舍入!然而,我们没有将0.666作为数字2/3的近似值的问题,所以我们不应该在大多数程序中如何逼近9.2。 (是的,在某些程序中它很重要。)
所以这里的数字基础是重要的。如果我们试图在基数3中代表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)
等)是不可能的!
这就是他们被称为非理性的原因。世界上没有多少比特存储就足以容纳其中一个。只有符号算术才能保持其精度。
虽然如果你将数学需求限制在有理数,但只有精度问题变得易于管理。你需要存储一对(可能是非常大的)整数a
和b
来保存由分数a/b
表示的数字。所有的算术都必须在分数上完成,就像在高中数学中一样(例如a/b * c/d = ac/bd
)。
当然,当涉及pi
,sqrt
,log
,sin
等时,你仍会遇到同样的麻烦。
TL; DR
对于硬件加速算术,只能表示有限数量的有理数。每个不可表示的数字都是近似的。无论系统如何,都不能表示某些数字(即无理数)。
有无数的实数(很多你不能枚举它们),并且有无限多的有理数(有可能枚举它们)。
浮点表示是有限的(就像计算机中的任何东西一样),因此不可避免地会有很多很多数字无法表示。特别是,64位只允许您区分18,446,744,073,709,551,616个不同的值(与无穷大相比无差别)。按照标准惯例,9.2不是其中之一。对于某些整数m和e,可能具有m.2 ^ e形式的那些。
您可能会想出一个不同的计算系统,例如10,其中9.2将具有精确的表示。但其他数字,比如1/3,仍然无法代表。
另请注意,双精度浮点数非常准确。它们可以表示任意数字,范围很广,最多可包含15个精确数字。对于日常生活计算,4或5位数就足够了。你永远不会真正需要那些15,除非你想要计算你一生中的每一毫秒。
为什么我们不能用二进制浮点表示9.2?
浮点数是(略微简化)具有有限数字位数和可移动小数点的位置编号系统。
如果分母的素数因子(当分数以其最低项表示时)是基数的因子,则只能使用位置编号系统中的有限位数来精确表示分数。
10的素因子是5和2,因此在基数10中我们可以表示形式a /(2b5c)的任何部分。
另一方面,2的唯一素因子是2,所以在基数2中我们只能表示形式的分数a /(2b)
为什么计算机使用这种表示?
因为它是一种简单的格式,并且对于大多数用途而言足够准确。基本上同样的原因,科学家们使用“科学记数法”并将结果四舍五入到每一步的合理位数。
当然可以使用(例如)32位分子和32位分母来定义分数格式。它将能够表示IEEE双精度浮点不能的数字,但同样会有许多数字可以用双精度浮点表示,这些浮点无法以这种固定大小的分数格式表示。
然而,最大的问题是这样的格式是计算上的痛苦。有两个原因。
有些语言确实提供了分数类型,但通常它们与仲裁精度相结合,这避免了需要担心近似分数但是它会产生它自己的问题,当一个数字经过大量的分母大小的计算步骤时因此,该部分所需的储存可能会爆炸。
有些语言也提供十进制浮点类型,这些类型主要用于以下情况:计算机获得的结果与预先存在的舍入规则相匹配(这主要是财务计算)。这些比二进制浮点更难以使用,但最大的问题是大多数计算机不为它们提供硬件支持。
试试这个
DecimalFormat decimalFormat = new DecimalFormat("#.##");
String.valueOf(decimalFormat.format(decimalValue))));
'decimalValue
'是你转换的价值。