为什么多次添加0.1仍然无损?


152

我知道0.1十进制数不能用有限的二进制数精确地表示(解释),因此double n = 0.1将失去一些精度,并且将不精确0.1。另一方面,0.5因为它确实可以表示0.5 = 1/2 = 0.1b

话虽如此,将0.1 三遍相加并不完全是可以理解的,0.3因此,以下代码将输出false

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

但是,0.1 五次相加会得到确切的结果0.5呢?以下代码显示true

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

如果0.1不能精确表示,将其相加5次给出的精确0.5表示又如何呢?


7
如果您真的进行了研究,我相信您可以弄清楚,但是浮点数充满了“意外”,有时最好只是好奇一下。
喜欢2014年

3
您正在以数学的方式考虑这个问题。浮点算术绝不是数学。
雅各布2014年

13
@HotLicks这是非常错误的态度。
hobbs 2014年

2
@RussellBorogove即使已被优化,但只有sum具有与真正执行循环相同的最终值时,它才是有效的优化。在C ++标准中,这称为“假设规则”或“相同的可观察行为”。
hobbs 2014年

7
@Jakob根本不是真的。浮点算术经过严格定义,对误差范围等进行了很好的数学处理。只是许多程序员不愿意继续进行分析,或者他们错误地认为,“浮点数是不精确的”是所有已知的知识,因此分析不值得打扰。
hobbs 2014年

Answers:


155

舍入误差不是随机的,并且它的实现方式会尝试使误差最小化。这意味着有时错误是不可见的,或者没有错误。

例如0.1是不完全0.1,即new BigDecimal("0.1") < new BigDecimal(0.1),但0.5也正是1.0/2

该程序向您显示所涉及的真实价值。

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

版画

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

注意:这0.3略有偏离,但是当您到达0.4位时,必须将其下移一位以适应53位的限制,并且错误将被丢弃。再次,一个错误蠕变回来为0.60.7,但对于0.81.0错误被丢弃。

将其添加5次应该累积错误,而不是取消错误。

出现错误的原因是由于精度有限。即53位。这意味着随着数字变大,数字将使用更多位,因此必须从末尾丢弃位。这会导致舍入,这种情况下对您有利。
当获得较小的数字(例如0.1-0.0999=>)时,您会得到相反的效果,1.0000000000000286E-4 并且看到的错误比以前更多。

这样的一个例子就是为什么在Java 6 中Math.round(0.49999999999999994)为什么返回1的原因,在这种情况下,计算中的一点损失会导致答案大不相同。


1
在哪里实施?
EpicPandaForce 2014年

16
@Zhuinden CPU遵循IEEE-754标准。Java使您可以访问基本的CPU指令,而无需参与。en.wikipedia.org/wiki/IEEE_floating_point
彼得Lawrey

10
@PeterLawrey:不一定是CPU。在CPU中没有浮点(并且没有使用单独的FPU)的机器上,IEEE算术将由软件执行。而且,如果主机CPU具有浮点但不符合IEEE要求,我认为该CPU的Java实现也必须使用软浮点...
R .. GitHub停止帮助ICE 2014年

1
@R ..在这种情况下,我不知道如果您使用strictfp Time来考虑定点整数,那会发生什么。(或BigDecimal)
彼得·劳瑞

2
@eugene的关键问题是浮点数可以表示的极限值。此限制可能导致信息丢失,并且随着数量的增加,也会丢失错误。它使用四舍五入,但在这种情况下,会四舍五入,因此本来是稍大的数字(如0.1太大)将变为正确的值。正好0.5
彼得·劳里

47

禁止溢出(以浮点数计x + x + x)恰好是正确舍入(即最接近)到实数3 *的浮点数xx + x + x + x正好是4 * x,并且x + x + x + x + x还是5 *的正确舍入的浮点近似值x

for的第一个结果x + x + x来自x + x确切的事实。x + x + x因此,仅是四舍五入的结果。

第二个结果比较困难,这里讨论一个证明(斯蒂芬·佳能暗示了对的最后三位数字进行案例分析的另一种证明x)。总而言之,3 * 与2 * x处于同一binade或3 * x与4 *处于相同binade x,并且在每种情况下都可以推断出第三次加法的错误抵消了第二次加法的错误(就像我们已经说过的那样,第一次添加是准确的。

第三个结果“ x + x + x + x + x正确舍入” 是从第二个结果中得出的,与第一个结果是由的正确性得出的相同x + x


第二个结果解释了为什么0.1 + 0.1 + 0.1 + 0.1恰好是浮点数0.4:将有理数1/10和4/10转换为浮点时,以相同的方式近似,具有相同的相对误差。这些浮点数之间的比率恰好为4。第一个和第三个结果表明0.1 + 0.1 + 0.10.1 + 0.1 + 0.1 + 0.1 + 0.1可以预期比纯朴的错误分析所推断的错误要少,但是,它们本身仅将结果分别3 * 0.1与和关联5 * 0.1,可以预期接近,但不一定与相同。0.30.5

如果您0.1在第四次加法之后继续加法,您将最终观察到舍入错误,这些错误使“ 0.1自身加n次”偏离n * 0.1,甚至偏离n / 10甚至更​​多。如果您将“ n次添加到自身的0.1次”的值绘制为n的函数,则您将观察到由Binade构成的恒定斜率线(一旦第n次加法的结果注定会落入特定Binade中,可以预期,添加的属性与以前在相同binade中产生结果的添加类似。在同一个binade中,误差将增大或减小。如果您要查看从Binade到Binade的坡度顺序,您会发现的重复数字0.1以二进制形式存在一段时间。在那之后,吸收将开始发生并且曲线将变得平坦。


1
在第一行中,您说的是x + x + x完全正确,但是从问题中的示例中可以看出是不正确的。
Alboz 2014年

2
@Alboz我说的x + x + x是正确舍入为实数3 *的浮点数x。在这种情况下,“正确舍入”是指“最近”。
Pascal Cuoq 2014年

4
+1这应该是公认的答案。它实际上提供了正在发生的事情的解释/证明,而不仅仅是模糊的概括。
R .. GitHub停止帮助ICE 2014年

1
@Alboz(所有问题均已预见)。但是,这个答案解释的是错误是如何幸运地消除而不是以最坏的情况累加。
hobbs 2014年

1
@chebus 0.1是十六进制的0x1.999999999999999999999…p-4(无限的数字序列)。它以双精度近似为0x1.99999ap-4。0.2是十六进制的0x1.999999999999999999999…p-3。由于相同的原因,0.1近似为0x1.99999ap-4,0.2近似为0x1.99999ap-3。同时,0x1.99999ap-3也是0x1.99999ap-4 + 0x1.99999ap-4。
Pascal Cuoq

-1

浮点系统具有各种神奇的功能,其中包括一些舍入精度。因此,由于不精确表示0.1而产生的非常小的误差最终四舍五入为0.5。

认为浮点数是一种很好的但不精确的数字表示方式。并非所有可能的数字都可以在计算机中轻松表示。非理性数字,例如PI。或像SQRT(2)。(符号数学系统可以表示它们,但我确实说“轻松”。)

浮点值可能非常接近,但并不精确。它可能太近了,您可以导航到Pluto并偏离毫米。但是从数学意义上讲仍然不是精确的。

当您需要精确而不是近似值时,请勿使用浮点数。例如,会计应用程序希望准确跟踪帐户中一定数目的便士。整数对此很有用,因为它们是精确的。您需要注意的整数问题是溢出。

对货币使用BigDecimal效果很好,因为基础表示形式是整数,尽管很大。

认识到浮点数是不精确的,它们仍然有很多用途。用于导航的坐标系或图形系统中的坐标。天文价值。科学价值观。(无论如何,您可能都不知道棒球的确切质量在电子的质量之内,因此不精确并不重要。)

对于计数应用程序(包括会计),请使用整数。要计算通过大门的人数,请使用int或long。


2
这个问题被标记为[java]。Java语言定义没有 “精确度的额外几位”的规定,仅提供了一些额外的指数位(仅当您不使用时strictfp)。仅仅因为您放弃了对某事的理解并不意味着它是不可思议的,也并不意味着其他人也应该放弃以理解某事。请参见stackoverflow.com/questions/18496560作为Java实现实现语言定义所用长度的示例(不包括任何额外的精度位规定,也不包含strictfp,任何额外的exp位)
Pascal Cuoq
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.