这部分是学术性的,至于我的目的,我只需将它四舍五入到小数点后两位;但是我很想知道结果会产生两个稍微不同的结果。
这是我写的测试,以缩小到最简单的实现:
@Test
public void shouldEqual() {
double expected = 450.00d / (7d * 60); // 1.0714285714285714
double actual = 450.00d / 7d / 60; // 1.0714285714285716
assertThat(actual).isEqualTo(expected);
}
但是输出失败了:
org.junit.ComparisonFailure:
Expected :1.0714285714285714
Actual :1.0714285714285716
任何人都可以详细解释引擎盖下的内容,导致1.000000000000000 X
的值不同吗?
我在答案中寻找的一些要点是: 精度在哪里丢失? 首选哪种方法,为什么? 哪个是正确的? (在纯数学中,两者都不对。也许两者都错了?) 这些算术运算有更好的解决方案或方法吗?
答案 0 :(得分:42)
我看到一堆问题告诉你如何解决这个问题,但没有一个真正解释发生了什么的问题,除了"浮点舍入误差很糟糕,m&#39 ;凯&#34?;那么让我来看看吧。我首先要指出的是,这个答案中没有任何内容特定于Java 。舍入误差是数字的任何固定精度表示所固有的问题,因此您在C中会遇到相同的问题。
作为一个简化示例,假设我们有某种本机使用无符号十进制数据类型的计算机,我们称之为float6d
。数据类型的长度为6位:4个专用于尾数,2个专用于指数。例如,数字3.142可以表示为
3.142 x 10^0
将以6位数字存储为
503142
前两位是指数加50,后四位是尾数。此数据类型可以表示从0.001 x 10^-50
到9.999 x 10^+49
的任何数字。
实际上,这不是真的。它无法存储任何号码。如果你想代表3.141592怎么办?还是3.1412034?还是3.141488906?幸运的是,数据类型不能存储超过四位数的精度,因此编译器必须对具有更多数字的任何内容进行舍入以适应数据类型的约束。如果你写
float6d x = 3.141592;
float6d y = 3.1412034;
float6d z = 3.141488906;
然后编译器将这三个值中的每一个转换为相同的内部表示3.142 x 10^0
(记住,存储为503142
),以便x == y == z
保持为真。 / p>
关键是有一整个实数范围都映射到相同的基础数字序列(或实际计算机中的位)。具体来说,任何x
满足3.1415 <= x <= 3.1425
(假设半偶数舍入)都会转换为表示503142
以便存储在内存中。
每次你的程序在内存中存储一个浮点值时,会发生这种舍入。第一次发生在你的源代码中写一个常量,正如我上面用x
,y
和z
所做的那样。每当您进行算术运算时,它会再次发生 ,这会增加精度数字的数量,超出数据类型所代表的数量。这些效果都称为roundoff error。有几种不同的方式可以实现:
加法和减法:如果您添加的其中一个值与另一个值的指数不同,您将获得额外的精度数字,如果有足够的数字,则最不重要需要放弃。例如,2.718和121.0都是可以在float6d
数据类型中精确表示的值。但是如果你试图将它们加在一起:
1.210 x 10^2
+ 0.02718 x 10^2
-------------------
1.23718 x 10^2
将四舍五入为1.237 x 10^2
或123.7,丢弃两位精度。
乘法:结果中的位数大约是两个操作数中位数的总和。这将产生一些舍入错误,如果您的操作数已经有许多有效数字。例如,121 x 2.718为您提供
1.210 x 10^2
x 0.02718 x 10^2
-------------------
3.28878 x 10^2
将四舍五入为3.289 x 10^2
或328.9,再次降低两位精度。
但是,要记住,如果你的操作数是“好的”,那就很有用了。数字,没有很多有效数字,浮点格式可能完全代表结果,所以你不必处理舍入错误。例如,2.3 x 140给出
1.40 x 10^2
x 0.23 x 10^2
-------------------
3.22 x 10^2
没有出现问题。
分部:这是事情变得混乱的地方。除非您划分的数字恰好是基数的幂(在这种情况下除法只是一个数字移位,或者除法),否则除非总是导致一定量的舍入错误二进制位移)。举一个例子,取两个非常简单的数字,3和7,除以它们,你得到
3. x 10^0
/ 7. x 10^0
----------------------------
0.428571428571... x 10^0
此数字的最接近的值可以表示为float6d
,4.286 x 10^-1
或0.4286,与确切结果明显不同。
正如我们将在下一节中看到的那样,舍入引入的错误会随着您执行的每个操作而增加。所以如果您正在使用&#34; nice&#34;数字,例如,通常最好尽可能晚地执行除法运算,因为这些操作最有可能将舍入错误引入您之前不存在的程序中。
一般情况下,如果你不能假设你的数字是“好的”,那么,舍入错误可能是正面的,也可能是负面的,并且很难预测它将朝哪个方向发展。只是基于操作。这取决于所涉及的具体价值。请查看2.718 z
的舍入误差图,作为z
的函数(仍然使用float6d
数据类型):
实际上,当您使用使用数据类型的完整精度的值时,通常更容易将舍入错误视为随机错误。查看该图,您可能会猜测误差的大小取决于操作结果的数量级。在这种特殊情况下,当z
的顺序为10 -1 时,2.718 z
也大约为10 -1 ,所以它将是0.XXXX
形式的一些形式。最大舍入误差是最后一位精度的一半;在这种情况下,通过&#34;最后一位精度&#34;我的意思是0.0001,因此舍入误差在-0.00005和+0.00005之间变化。在2.718 z
跳到下一个数量级的点,即1 / 2.718 = 0.3679时,您可以看到舍入误差也会跳跃一个数量级。
您可以使用众所周知的techniques of error analysis来分析某个幅度的随机(或不可预测)错误如何影响您的结果。具体来说,对于乘法或除法,&#34;平均值&#34;结果中的相对误差可以通过在每个操作数中添加正交中的相对误差来近似 - 也就是说,将它们加起来,加上它们,然后取平方根。对于我们的float6d
数据类型,相对误差在0.0005(对于像0.101这样的值)和0.00005(对于像0.995这样的值)之间变化。
让我们将0.0001作为值x
和y
中相对误差的粗略平均值。 x * y
或x / y
中的相对错误由
sqrt(0.0001^2 + 0.0001^2) = 0.0001414
是sqrt(2)
的因子,大于每个单独值的相对误差。
在组合操作时,您可以多次应用此公式,每次浮点运算一次。例如,对于z / (x * y)
,x * y
中的相对误差平均为0.0001414(在此十进制示例中),然后z / (x * y)
中的相对误差为
sqrt(0.0001^2 + 0.0001414^2) = 0.0001732
请注意,平均相对误差随着每次操作而增加,特别是作为乘法和除法的平方根。
同样,对于z / x * y
,z / x
中的平均相对误差为0.0001414,z / x * y
中的相对误差为
sqrt(0.0001414^2 + 0.0001^2) = 0.0001732
所以,同样,在这种情况下。这意味着对于任意值,平均而言,这两个表达式引入了大致相同的错误。 (从理论上讲,就是这样。我已经看到这些操作在实践中表现得非常不同,但这是另一个故事。)
您可能对您在问题中提供的特定计算感到好奇,而不仅仅是平均值。对于该分析,让我们切换到二进制算术的真实世界。大多数系统和语言中的浮点数用IEEE standard 754表示。对于64位数字,format指定52位专用于尾数,11位指定指数,1位指定符号。换句话说,当写入基数2时,浮点数是形式的值
1.1100000000000000000000000000000000000000000000000000 x 2^00000000010
52 bits 11 bits
未明确存储前导1
,并构成第53位。此外,您应该注意,存储以表示指数的11位实际上是实指数加上1023.例如,此特定值为7,即1.75 x 2 2 。尾数是二进制的1.75,或1.11
,二进制的指数是1023 + 2 = 1025,或10000000001
,因此存储在内存中的内容是
01000000000111100000000000000000000000000000000000000000000000000
^ ^
exponent mantissa
但这并不重要。
您的示例还涉及450,
1.1100001000000000000000000000000000000000000000000000 x 2^00000001000
和60,
1.1110000000000000000000000000000000000000000000000000 x 2^00000000101
您可以使用this converter或互联网上的任何其他值来使用这些值。
当你计算第一个表达式450/(7*60)
时,处理器首先进行乘法,得到420,或
1.1010010000000000000000000000000000000000000000000000 x 2^00000001000
然后它将450除以420.这产生15/14,这是
1.0001001001001001001001001001001001001001001001001001001001001001001001...
二进制。现在,the Java language specification说明了
不精确的结果必须四舍五入到最接近无限精确结果的可表示值;如果两个最接近的可表示值相等,则选择具有最低有效位的值。这是IEEE 754标准的默认舍入模式,称为舍入到最近。
,64位IEEE 754格式的最接近的可表示值为15/14
1.0001001001001001001001001001001001001001001001001001 x 2^00000000000
小数约为1.0714285714285714
。 (更确切地说,这是唯一指定此特定二进制表示的最不精确的十进制值。)
另一方面,如果先计算450/7,结果为64.2857142857 ...,或者是二进制,
1000000.01001001001001001001001001001001001001001001001001001001001001001...
,其最接近的可表示值为
1.0000000100100100100100100100100100100100100100100101 x 2^00000000110
这是64.28571428571429180465 ...请注意由于舍入误差导致二进制尾数的最后一位(与精确值相比)的变化。将此除以60可以得到
1.000100100100100100100100100100100100100100100100100110011001100110011...
看结尾:模式不同!重复的是0011
,而不是像其他情况那样001
。最接近的可表示值是
1.0001001001001001001001001001001001001001001001001010 x 2^00000000000
与最后两位的其他操作顺序不同:它们是10
而不是01
。十进制当量是1.0714285714285716。
如果您查看确切的二进制值,则应该清楚导致此差异的特定舍入:
1.0001001001001001001001001001001001001001001001001001001001001001001001...
1.0001001001001001001001001001001001001001001001001001100110011001100110...
^ last bit of mantissa
在这种情况下,前者的结果(数字15/14)恰好是精确值的最准确表示。这是一个如何让分工直到最后让您受益的一个例子。但同样,只要您使用的值不使用数据类型的完整精度,此规则就会保留。一旦开始使用不精确(舍入)值,您就不再通过先进行乘法来保护自己免受进一步的舍入错误。
答案 1 :(得分:5)
它与double
类型的实现方式以及浮点类型与其他更简单的数字类型不具有相同的精度保证这一事实有关。虽然下面的答案更具体地说是总和,但它也通过解释在浮点数学运算中如何不能保证无限精度来回答你的问题:Why does changing the sum order returns a different result?。基本上,在不指定可接受的误差范围的情况下,您永远不应该尝试确定浮点值的相等性。 Google的Guava库包含DoubleMath.fuzzyEquals(double, double, double)
,用于确定一定精度内两个double
值的相等性。如果您希望了解浮点平等的具体细节this site is quite useful;同一网站also explains floating-point rounding errors。总之:由于操作顺序的计算之间的舍入不同,计算的预期值和实际值会有所不同。
答案 2 :(得分:4)
让我们稍微简化一下。您想知道的是450d / 420
和450d / 7 / 60
(具体)给出不同结果的原因。
让我们看看如何在IEE双精度浮点格式中执行除法。在不深入了解实现细节的情况下,它基本上XOR
- 使用符号位,从被除数的指数中减去除数的指数,除以尾数,并对结果进行归一化。
首先,我们应该以{{1}}:
的正确格式表示我们的数字double
首先将450 is 0 10000000111 1100001000000000000000000000000000000000000000000000
420 is 0 10000000111 1010010000000000000000000000000000000000000000000000
7 is 0 10000000001 1100000000000000000000000000000000000000000000000000
60 is 0 10000000100 1110000000000000000000000000000000000000000000000000
除以450
首先是符号位,它是420
(0
)。
然后是指数。 0 xor 0 == 0
看起来很好,现在是尾数:
10000000111b - 10000000111b + 1023 == 10000000111b - 10000000111b + 01111111111b == 01111111111b
。有几种不同的方法可以做到这一点,我稍后会谈谈它们。结果是1.1100001000000000000000000000000000000000000000000000 / 1.1010010000000000000000000000000000000000000000000000 == 1.1100001 / 1.101001
(您可以验证here
)。
现在我们应该将结果标准化。让我们看看守卫,圆形和粘性位值:
1.0(001)
保护位0,我们不进行任何舍入。结果是,二进制:
0001001001001001001001001001001001001001001001001001 0 0 1
以小数形式表示为0 01111111111 0001001001001001001001001001001001001001001001001001
。
现在让我们将1.0714285714285714
除以450
进行类推。
签名位= 7
指数= 0
尾数= 10000000111b - 10000000001b + 01111111111b == -01111111001b + 01111111111b + 01111111111b == 10000000101b
舍入:
1.1100001 / 1.11 == 1.00000(001)
保护位置位,圆形和粘滞位不置位。我们四舍五入到最近(IEEE的默认模式),我们正处于可以舍入的两个可能值之间。由于lsb为0000000100100100100100100100100100100100100100100100 1 0 0
,我们会添加0
。这给了我们圆形的尾数:
1
结果是
0000000100100100100100100100100100100100100100100101
以小数形式表示为0 10000000101 0000000100100100100100100100100100100100100100100101
。
现在我们必须把它除以64.28571428571429
......但是你已经知道我们已经失去了一些精确度。将60
除以450
根本不需要舍入,但在这里,我们必须至少对结果进行一次。但是,为了完整起见,让我们完成这项工作:
将420
除以64.28571428571429
签名位= 60
指数= 0
尾数= 10000000101b - 10000000100b + 01111111111b == 01111111110b
回合和转变:
1.0000000100100100100100100100100100100100100100100101 / 1.111 == 0.10001001001001001001001001001001001001001001001001001100110011
与前一种情况一样,我们得到尾数:0.1000100100100100100100100100100100100100100100100100 1 1 0 0
1.0001001001001001001001001001001001001001001001001001 1 0 0
。
当我们转移0001001001001001001001001001001001001001001001001010
时,我们将其添加到指数中,获取
指数= 1
所以,结果是:
01111111111b
以小数形式表示为0 01111111111 0001001001001001001001001001001001001001001001001010
。
<强>铊组成; dr 强>:
第一师给了我们:
1.0714285714285716
最后一个部门给了我们:
0 01111111111 0001001001001001001001001001001001001001001001001001
差异只在最后2位,但我们可能会失去更多 - 毕竟,为了获得第二个结果,我们不得不围绕两次而不是没有!
现在,关于尾数分裂。浮点除法有两种主要方式实现。
IEEE长划分(here规定的方式是一些很好的例子;它基本上是常规的长除法,但是用二进制而不是十进制),而且它很慢。这就是你的电脑所做的。
还有一个更快,但收益更少的选项,乘以逆。首先,找到除数的倒数,然后进行乘法。
答案 3 :(得分:1)
那是因为双重划分经常会导致精度下降。所述损失可以根据划分的顺序而变化。
除以7d
时,您已经失去了实际结果的精确度。然后只有你将错误的结果除以60
。
除以7d * 60
时,只需使用一次除法,因此只会丢失一次精度。
请注意,双倍乘法有时也会失败,但这种情况不常见。
答案 4 :(得分:0)
当然,操作的顺序与双倍精确的事实混合在一起:
450.00d / (7d * 60) --> a = 7d * 60 --> result = 450.00d / a
VS
450.00d / 7d / 60 --> a = 450.00d /7d --> result = a / 60