我看到一堆问题告诉你如何解决这个问题,但除了“浮点舍入误差不好,好吧?”之外,没有一个问题能真正解释发生了什么。所以让我试一试。首先让我指出 此答案中没有任何内容是 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 将成立。
关键在于,有一整套实数都映射到相同的底层数字序列(或位,在真实计算机中)。具体来说,任何满足3.1415 <= x <= 3.1425(假设为半偶数舍入)的x 都会转换为表示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
没有舍入问题。
-
Division:这就是事情变得混乱的地方。除法几乎总是会导致一定量的舍入误差,除非您要除以的数字恰好是基数的幂(在这种情况下,除法只是数字移位或位移二进制)。例如,取两个非常简单的数字,3 和 7,将它们相除,得到
3. x 10^0
/ 7. x 10^0
----------------------------
0.428571428571... x 10^0
可以表示为float6d 的与此数字最接近的值是4.286 x 10^-1,或0.4286,这与确切的结果明显不同。
正如我们将在下一节中看到的,舍入带来的误差会随着您执行的每个操作而增加。所以如果您使用“不错”的数字(如您的示例中所示),通常最好尽可能晚地进行除法运算,因为这些运算最有可能在您的程序中引入舍入误差以前不存在的地方。
舍入误差分析
一般来说,如果您不能假设您的数字“不错”,则舍入误差可以是正数也可以是负数,并且仅根据运算很难预测它会朝哪个方向发展。这取决于所涉及的具体值。查看 2.718 z 作为 z 函数的舍入误差图(仍然使用 float6d 数据类型):
实际上,当您处理使用数据类型的完整精度的值时,通常更容易将舍入误差视为随机误差。查看该图,您可能会猜到误差的大小取决于运算结果的数量级。在这种特殊情况下,当z 的顺序为 10-1 时,2.718 z 的顺序也为 10-1,因此它将是一个数字0.XXXX 的形式。最大舍入误差是最后一位精度的一半;在这种情况下,“精度的最后一位”是指 0.0001,因此舍入误差在 -0.00005 和 +0.00005 之间变化。在2.718 z 跃升到下一个数量级的点,即 1/2.718 = 0.3679,您可以看到舍入误差也跃升了一个数量级。
您可以使用众所周知的techniques of error analysis 来分析某个量级的随机(或不可预测的)错误如何影响您的结果。具体来说,对于乘法或除法,您的结果中的“平均”相对误差可以通过将每个操作数中的相对误差相加 正交 来近似 - 也就是说,将它们平方,相加,然后取平方根。对于我们的 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 22。尾数为二进制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)恰好是精确值的最准确表示。这是一个例子,说明离开分裂直到结束如何使您受益。但同样,该规则仅在您使用的值不使用数据类型的完整精度时才成立。一旦开始使用不精确(四舍五入)的值,您将不再通过先进行乘法来保护自己免受进一步的四舍五入错误。