由于浮点不精确,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
Run Code Online (Sandbox Code Playgroud)
我很好奇最后一行如何产生精度误差?我假设表达式的计算结果为:((d1+d2)-d2)-d1。因此,即使 d1、d2 或 (d1+d2) 没有准确表示,我也会假设精度误差会相互抵消?
听起来您对 IEEE754 浮点数学的工作原理感到困惑(这是 java 中的工作方式double和float行为 - 根据 IEEE754 规范)。这并不奇怪;总而言之,这是一个相当复杂的系统!
有一些易于理解的心理模型应该会有所帮助。一旦了解了这些,您应该能够凭直觉找到问题的答案。
我有一个挑战要给你。拿一张小纸,用十进制记下 1 除以 5 的值。这应该很简单,只需写下“0.2”即可。
现在将其翻过来并写下 1 除以 3。
你做不到。您可以尝试接近,具体取决于您的纸的大小,您最终得到“0.333333333333333”,但随后您就没有空间了。无论你的论文有多大,考虑到你必须以小数形式写下来而不是分数的限制,你都无法完成这项工作。
那么3对5有什么特别之处呢?
诀窍是,5 是我们在十进制中使用的基数的公因数:5 可以被 10 整除,十进制系统是以 10 为底。出于同样的原因,1/2 有效(0.5),甚至 3/4 也有效(当你将 4 分解为质因数时,它是2*2- 并且所有这些都可以被我们的基数 10 整除。这就是为什么 3/4 可以完美地用十进制表示)。但是 1/7 不起作用,就像 1/6 不起作用一样(6 分解为2*3- 3 不能干净地整除 10,所以,你不能干净地用十进制写出 1/6)。
不过,计算机以二进制计数,这立即导致令人讨厌的意外:十进制中唯一的公因数是 2 本身。因此,IEEE754 数学可以准确地表示例如 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....???
Run Code Online (Sandbox Code Playgroud)
尝试使用相同的欺骗手段0.75,你永远不会得到奇怪的结果,因为 0.25 在 IEEE754 中也表现得很好。
换句话说,“0.1”(1/10)在我们的十进制系统中工作得很好,但在二进制中它和十进制中的 1/3 一样有问题:我们的纸上没有“空间”,所以计算机必须围绕它。
计算机是二进制野兽,CPU 喜欢固定宽度的概念 - 计算机只能存储零和一,它们不能存储其他任何东西,甚至不能存储东西有多大,所以除非外部因素决定了东西有多大,否则计算机不知道去哪里看。当在纸上书写数字时,我们使用数字 0 到 9,但我们使用的不止这些:我们使用点表示分数国家(例如 1.75),并使用页面上的空格来指示事物的开始和结束位置。计算机不具备这些功能,因此除非您想使用 0 和 1 来存储数据的大小,否则最简单的方法就是规定该数字为 64 位。现在我们知道从哪里开始和停止。
给定 64 位,您最多只能表示2^64唯一的数字。这是很多数字,但不是无限的。
想象一条从负无穷到正无穷的数轴。这double至少在理论上是能够代表的。0 到 1 之间有无数个数字,更不用说跨越那条数轴了。
然而,其中的 0.0000000000% 可以用双精度数准确表示。毕竟,我们的 double 最多只能表示 2^64 个数字,而我们的数轴却有无限的范围。
那么如何double运作呢?首先,它选择略少于 2^64 的唯一数字。想象一下我们在数轴上扔了那么多飞镖。我们称这些为“祝福数字”——double只能代表飞镖,不能代表任何其他数字。然后 IEEE754 制定了一个非常简单的法令:任何数字(因此,任何输入、任何中间结果)总是默默地四舍五入到最接近的 dart。
换句话说,如果我们调用dart(x)将结果四舍五入到最接近的 dart 的函数,则写入会d1 + d2 - d2 - d1分解为(((d1 + d2) - d2) - d1),从而变成dart(dart(dart(dart(d1) + dart(d2)) - dart(d2)) - dart(d1))。这是很多飞镖!!
飞镖分布不均匀。事实上,一半的飞镖非常接近 0)。当您远离零时,任何两个飞镖之间的距离会变得越来越大。
在 2^53 处,2 个省之间的距离开始超过 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
Run Code Online (Sandbox Code Playgroud)
到目前为止,一切都很好——System.out.println代码以指数表示法打印大双精度数,但它是完全正确的,如“将其转换回长整型”所示。但现在...
double c = b + 1;
sysout(c); // 9.007199254740992E15
sysout(b == c); // true - wait what?
Run Code Online (Sandbox Code Playgroud)
加 1没有任何作用。b 和 c 实际上仍然是相同的值。dart(b + 1)与 b 相同,因为 2^53 确实是最接近 2^53+1 计算结果的省道。
double d = b - 1;
sysout(d); // 9.007199254740991E15
sysout(b == d); // false
Run Code Online (Sandbox Code Playgroud)
但往下走一趟确实效果很好。
现在我们可以简单地解释一下你的问题:
d1和省道加d2在一起,这个数字并不完全适合省道(为什么会这样 - 很少这样做)。所以它也被四舍五入到最接近的省份。d2再次减去 darted,然后对结果进行 darted。这并不能让我们得到相同的数字。这应该不会太令人惊讶 - 飞镖分布不均匀 - 将 d1 和 d2 加在一起会移动到数轴上“飞镖密度较低”的区域。落镖密度越低意味着准确度越低。回避更高的数字(加上 d2,然后再次减去 - 在这中间,我们处于飞镖密度较低的区域)会让我们付出代价。d1从你所拥有的中减去。鉴于我们现在正在接近 0,这里有很多省份,因此省份舍入不会让我们回到 0,但会让我们得到一些非常接近 0 的省份。当然,理论上 java 可能已经消失了:哦,嘿,,d1+d2-d2-d1我可以摆脱d2-d2,然后离开d1-d1并摆脱那个,然后离开0,但这不是它的工作原理:java 规范明确指出了这一点+并且-是二元运算符(不是任意数量运算符),并且它们是从左到右解析的。Java 不是数学家在白板上简化公式。Java 只是按照规范的要求去做。即将其分解为(((d1+d2)-d2)-d1),然后应用 IEEE753 (即对所有内容四舍五入到最近的 dart)。
| 归档时间: |
|
| 查看次数: |
118 次 |
| 最近记录: |