为什么十进制小数不能在二进制中被精确表示?

314
在SO上发布了几个关于浮点表示的问题,例如小数0.1没有精确的二进制表示,因此使用“==”运算符将其与另一个浮点数进行比较是危险的。我理解浮点表示背后的原理。但我不明白的是,从数学角度讲,为什么小数点右侧的数字比左侧的数字更加“特殊”?例如,数字61.0具有精确的二进制表示,因为任何数字的整数部分始终是精确的。但数字6.10就不是精确的了。我只是把小数点向右移动了一位,突然间从精确到不精确。在数学上,这两个数字之间应该没有内在的区别--它们只是数字而已。相反,如果我向左移动小数点一位,得到数字610,我仍然处于精确状态。我可以一直朝着这个方向走(6100、610000000、610000000000000),它们仍然是精确的。但是一旦小数点超过某个阈值,数字就不再精确了。发生了什么?编辑:为了澄清,我想避免讨论诸如IEEE等行业标准表示法,并坚持我认为的数学“纯粹”方式。 在十进制中,位值是:
... 1000  100   10    1   1/10  1/100 ...

在二进制中,它们将会是:

... 8    4    2    1    1/2  1/4  1/8 ...

这些数字也没有任何限制。数字的位置从左到右无限增加。


2
你可能会发现这篇文章有助于理解浮点数内部的运作方式:浮点数的解剖 - John D. Cook
61
在二进制中,数字3表示为2¹+2°=2+1。非常简单易懂。现在,看一下1/3。你如何使用负幂次的2来表示它?试试实验,你会发现1/3等于无限序列2^-2 + 2^-4 + 2^-6 + 2^-8 + ...的总和,也就是说,在二进制中很难精确地表示。 - Lars Haugseth
26
Jon Skeet在你的问题中提供了很好的回答。但有一件事情被遗漏了,那就是你实际上问了两个不同的问题。标题问题是“为什么十进制小数不能在二进制中精确表示?”答案是,它们可以被表示。在你的标题和正文之间,你混淆了“二进制”和“浮点表示”的概念。浮点数是一种以固定数量的二进制数字表示十进制数的方法,但会牺牲精度。而二进制只是另一种计数法,可以表示任何十进制数,只要有无限多的数字。 - Chris Blackwell
3
有一些系统具有精确的十进制表示,其工作方式基本上与您描述的相同。 SQL十进制类型就是一个例子。LISP语言内置了这个功能。有几个商用和开源库可用于使用精确的小数计算。只是目前还没有此类硬件支持,大部分语言和硬件都实现了IEEE标准,以32或64位表示无限数量的数字。 - nos
2
该问题似乎与数学有关(即使它是涉及编程的数学),更适合在 [math.se] 上进行讨论。 - Cole Tobin
22个回答

2
在这个方程中,
2^x = y ;  
x = log(y) / log(2)

因此,我在想我们是否可以为二进制引入一个对数基数系统,例如:
 2^1, 2^0, 2^(log(1/2) / log(2)), 2^(log(1/4) / log(2)), 2^(log(1/8) / log(2)),2^(log(1/16) / log(2)) ........

这可能能够解决问题,所以如果你想要将32.41写成二进制,那么它应该是:

2^5 + 2^(log(0.4) / log(2)) + 2^(log(0.01) / log(2))

或者

2^5 + 2^(log(0.41) / log(2))

1
问题是你不知道这个数字是否确实是61.0。请考虑以下内容:


float a = 60;
float b = 0.1;
float c = a + b * 10;

c的值是多少?它并不完全等于61,因为b实际上并不是0.1,因为0.1在二进制中没有精确的表示。


1

数字61.0确实具有精确的浮点运算,但并非对于所有整数都是如此。如果您编写了一个循环,将双精度浮点数和64位整数都加1,最终会达到一个点,其中64位整数完美地表示了一个数字,但浮点数却没有,因为它没有足够的有效位。

在小数点右侧,接近值的点就容易得多。如果开始用二进制浮点数写出所有数字,这就更有意义了。

另一种思考方式是,当您注意到数字61.0在十进制中可以完美表示时,并且移动小数点不会改变这一点,您正在执行10的乘方(10^1,10^-1)乘法。在浮点数中,乘以2的幂不会影响数字的精度。尝试重复将61.0除以3,以说明一个完全准确的数字如何失去其精确表示。


1

这里有一个阈值,因为数字的含义已经从整数变成了非整数。要表示61,您需要6*10^1 + 1*10^0;10^1和10^0都是整数。6.1是6*10^0 + 1*10^-1,但10^-1是1/10,绝对不是整数。这就是你最终进入Inexactville的原因。


1
可以将分数和整数进行类比。例如,一些分数如1/7在十进制形式下需要很多很多位小数才能表示出来。由于浮点数是基于二进制的,所以特殊情况会有所改变,但同样的精度问题也会出现。

1

你无法在二进制中精确地表示0.1,就像你无法使用传统的英寸标尺来测量十分之一英寸一样。

英寸标尺和二进制小数都是关于一半的。你可以测量半英寸、四分之一英寸(当然是半个半英寸)、八分之一英寸、十六分之一英寸等。

但如果你想要精确地测量十分之一英寸,那么你就没那么幸运了。它比八分之一英寸少,但比十六分之一英寸多。如果你想得更准确,你会发现它比3/32略大一点,但比7/64略小一点。我从未见过实际上有比64份之一还细的刻度的标尺,但如果你做一下数学,你会发现1/10比13/128小,而比25/256大,并且比51/512大。你可以继续用1024分之一、2048分之一、4096分之一、8192分之一等更细的单位来测量,但是即使在一个无限细的基于二的标尺上,你也永远找不到与1/10或0.1完全对应的明确标记。

你会发现一些有趣的东西。让我们来看看我列出的所有近似值,并为每个值明确记录0.1是更小还是更大:

分数 小数 0.1 是... 转化为 0/1
1/2 0.5 小于 0
1/4 0.25 小于 0
1/8 0.125 小于 0
1/16 0.0625 大于 1
3/32 0.09375 大于 1
7/64 0.109375 小于 0
13/128 0.1015625 小于 0
25/256 0.09765625 大于 1
51/512 0.099609375 大于 1
103/1024 0.1005859375 小于 0
205/2048 0.10009765625 小于 0
409/4096 0.099853515625 大于 1
819/8192 0.0999755859375 大于 1
现在,如果你看最后一列,你会看到0001100110011。这并不是巧合,无限重复的二进制分数1/10就是0.0001100110011...

0

正如我们一直在讨论的那样,在浮点算术中,十进制数0.1无法在二进制中完美表示。

浮点和整数表示为所表示数字提供了网格或晶格。随着算术运算的进行,结果会从网格上掉落,并且必须通过四舍五入将其放回到网格上。例如,在二进制网格上的1/10。

如果我们使用二进制编码的十进制表示法,就像有位先生建议的那样,我们是否能够将数字保持在网格上?


1
十进制数没问题。但这只是定义而已。你不能用十进制表示1/3,就像你不能用二进制表示0.1一样。任何量化方案都无法处理无限大的数字集合。 - Kylotan

0

1
但是即使有无限数量的位数,如果您使用浮点“二进制”小数点,您仍然无法精确表示0.1,就像您在十进制中即使有无限数量的位数也无法精确表示1/3一样。 - Jon Skeet
3
@Jon,那是不正确的:有了无限数量的小数,例如我可以准确地表达“三分之一”。现实世界的问题在于,物理上不可能拥有“无限多”的小数或位数。 - ChrisW
1
刚加入的朋友们,可以参考维基百科上对于“0.9999999999…”的文章。 - Steve Summit

0

你知道整数吧?每个位代表2^n。


2^4=16
2^3=8
2^2=4
2^1=2
2^0=1

对于浮点数也是一样的(有些区别),但是位代表2^-n。 2^-1=1/2=0.5
2^-2=1/(2*2)=0.25
2^-3=0.125
2^-4=0.0625

浮点二进制表示:

符号  指数    小数(我认为小数后面会添加一个看不见的1)
B11  B10 B9 B8   B7 B6 B5 B4 B3 B2 B1 B0


0

上面得分很高的答案说得很好。

首先,在你的问题中混合了二进制和十进制,然后当你在右侧放置一个不能被基数整除的数字时,就会出现问题。比如十进制中的1/3,因为3不能被10的幂整除,或者二进制中的1/5,它不能被2的幂整除。

另外,永远不要在浮点数中使用等号,即使它是一个精确的表示,某些浮点系统中的一些数字可以用多种方式准确地表示(IEEE在这方面做得很糟糕,它是一个可怕的浮点规范,所以请预料头痛)。在这里也是一样,1/3不等于计算器上的0.3333333,无论小数点右边有多少个3。它可能足够接近,但并不相等。因此,你会期望像2*1/3这样的表达式不等于2/3,具体取决于四舍五入。永远不要在浮点数中使用等号。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接