java双精度错误时,添加和减去相同的数字

fykwrbwg  于 2023-05-21  发布在  Java
关注(0)|答案(2)|浏览(180)

由于浮点数的不精确性,java的double不能精确地表示每一个十进制数。引入了精度误差。下面是一个例子:

double d1=0.20976190476190476;
double d2=0.062142857142857146;
System.out.println("d1-d1= "+(d1-d1)); //Prints 0.0.
System.out.println("d1+d2-d2-d1= "+(d1+d2-d2-d1)); //Prints 2.7755575615628914E-17

我很好奇最后一行怎么会产生精度误差?我假设表达式的求值为:((dl +d2)-d2)-dl。因此,即使d1、d2或(d1+d2)没有精确表示,我也会假设精度误差会相互抵消?

u2nhd7ah

u2nhd7ah1#

听起来你对IEEE 754浮点数学的工作原理感到困惑(根据IEEE 754规范,这就是doublefloat在java中的行为)。这...并不奇怪总而言之,这是一个相当复杂系统!
有一些容易理解的心理模型应该会有所帮助。一旦你知道了这些,你应该能够凭直觉回答你的问题。

记住,计算机是十进制的

我有个挑战给你拿一小张纸,用十进制记法写下1除以5的值。这应该很简单,你只需要写下“0.2”就可以了。
现在把它翻过来,写下1除以3。
你不能这么做你可以试着接近,根据你的纸的大小,你最终得到“0.33333333333333”,但你用完了空间。不管你的论文有多大,* 鉴于你必须用十进制而不是分数 * 来写的限制,你不能做这项工作。
三对五有什么特别的?
诀窍是,5是我们在十进制中使用的基数的公因子:5可以被10整除,十进制以10为基数。出于同样的原因,1/2也可以(0.5),甚至3/4也可以(当你把四个分解成素因子时,它是2*2-并且所有这些都可以被我们的基数10整除。这就是为什么3/4ths在十进制中完美地“工作”)。但是1/7不起作用,就像1/6不起作用一样(6分解为2*3-3不能被干净地整除为10,所以,你不能在十进制中干净地写1/6)。
计算机以二进制计数,这立即导致了一个令人讨厌的惊喜:十进制中唯一的公因数是2本身。因此,IEEE 754数学可以准确地表示例如1/8甚至7/4096,但它相当有限。1/3、1/5、1/7、1/9,甚至1/10都不行。只有2的倍数'工作'没有小数错误。我们可以简单地观察到:

double x = 0;
for (int i = 0; i < 10; i++) x += 0.1;
System.out.println(1.0 == x); // prints 'false'. What the....???

用同样的诡计。0.75,你永远不会得到不稳定的结果,因为0.25在IEEE 754中也能很好地工作。
换句话说,“0.1”(1/10)在我们的十进制系统中工作得很好,但在二进制中,它就像十进制中的1/3一样有问题:我们在纸上用完了“空间”,所以计算机必须将其舍入。

数字行有偏差。

计算机是二进制的野兽,CPU喜欢固定宽度的概念--计算机只能存储0和1,它们不能存储其他任何东西,甚至不能存储东西有拉格,所以除非有外部因素决定了东西有多大,否则计算机不知道该去哪里看。当我们在纸上写数字时,我们有数字0到9,但我们使用的不止这些:我们用一个点来表示分数(例如1.75),我们在页面上使用间距来表示事情的开始和结束。计算机没有这些,所以除非你想用0和1来存储大小,最简单的方法就是命令这个数字是64位的。现在我们知道从哪里开始和停止了。
给定64位,最多只能表示2^64个唯一数字。这是一个 * 很多 * 的数字,但不是无限的数量。
想象一条从负无穷到正无穷的数线。这是double至少在理论上能够表示的。在0和1之间有无限多的数字,更不用说跨越这条数字线了。
然而,其中的0.00000000000%可以用双精度表示。毕竟,我们的double最多只能表示2^64个数字,而我们的数字线有无限的扩展。
double是如何工作的?首先,它选择略少于2^64个唯一数字。想象一下,我们在数字线上扔了那么多 dart 。我们称之为“受祝福的数字”-double只能代表 dart ,而不是任何其他数字。然后IEEE 754做出了一个非常简单的命令:任何数字(因此,任何输入,任何中间结果)总是默默四舍五入到最近的 dart 。
换句话说,如果我们调用dart(x)函数将结果舍入到最近的箭头,则写入d1 + d2 - d2 - d1将分解为(((d1 + d2) - d2) - d1),这将变成dart(dart(dart(dart(d1) + dart(d2)) - dart(d2)) - dart(d1))。* 这是很多 dart !!*
dart 不是均匀分布的。事实上,有一半的 dart 非常接近0)。当你从零开始移动时,任何两个 dart 之间的距离都会越来越大。
在2^53处,两个省道之间的距离开始超过1.0。2^53?这是一个受祝福的数字。事实上,从0到2^53的所有整数都是有福的。但是2^53 + 1呢?不受祝福。让我们试试吧!

long a = 2L << 52;
sysout(a); // 9007199254740992

double b = Math.pow(2.0, 53);
sysout(b); // 9.007199254740992E15
sysout((long) b); // 9007199254740992
sysout(a == (long) b); // true

到目前为止,一切都很好--System.out.println代码以指数表示法打印了大的双精度数,但它是完全正确的,正如“将其转换为long”所示。但现在...

double c = b + 1;
sysout(c); // 9.007199254740992E15
sysout(b == c); // true - wait what?

添加1不起任何作用。B和c实际上仍然是相同的值。dart(b + 1)与B相同,因为2^53确实是计算2^53+1的结果最接近的 dart 。

double d = b - 1;
sysout(d); // 9.007199254740991E15
sysout(b == d); // false

但是一个人下去确实很好。

解释您的问题

现在我们可以简单地解释你的问题:

  • 您的2个数字四舍五入到最近的 dart 。没问题。

  • 然后我们把省道d1和省道d2加在一起,这个数字并不完全适合省道(为什么会这样-它很少这样)。这也是最近的距离。

  • 然后我们再次减去darted d2,然后得出dart结果。我们的号码不一样。这不应该太令人惊讶- dart 不是均匀分布的-将d1和d2加在一起会移动到数字线的“ dart 密度较低”的区域。较少的省道密度意味着较低的准确性。回避到更高的数字(加上d2,然后再减去-在中间,我们在 dart 密度较低的区域)花费我们。

  • 最后,你减去d1从你有。考虑到我们现在正在接近0,这里有很多 dart ,所以 dart 舍入不会让我们回到0,而是让我们到达一些非常接近0的 dart 。

当然,从理论上讲,Java可以这样做:哦,嘿,d1+d2-d2-d1,我可以去掉d2-d2,然后剩下d1-d1,然后把它去掉,剩下0,但这不是它的工作方式:java规范明确指出+-是二元运算符(不是任何数量运算符),它们是从左到右解析的。Java不是数学家在白板上简化公式。Java只是做规范说它做的事情。也就是将其分解为(((d1+d2)-d2)-d1),然后应用IEEE 753(即圆到最近的 dart 为所有的事情)。

f0ofjuux

f0ofjuux2#

jshell(interactive java shell):jshell> d1+d2-d2 $6 ==> 0.2097619047619048
它不等于d1,因此您可以快速确认错误不会相互抵消。
浮点数的奇怪行为的一个极端例子是将一个非常小的量添加到一个非常大的量上-这个加法甚至可能不会改变大值,因此通常d1 + d2可以等于d1,即使d2不为零。
浮点数学是棘手的,其中数值精度非常重要,请确保您使用/理解手头的算法,使用Maven编写的库,或使用BigDecimal或其他任意精度的数字。

相关问题