使用双精度时,为什么不(x /(y*z))与(x/y/z)相同?

Ben*_*son 22 java double rounding operator-precedence double-precision

这部分是学术性的,就我的目的而言,我只需将它四舍五入到小数点后两位; 但是我很想知道结果会产生两个稍微不同的结果.

这是我写的测试,以缩小到最简单的实现:

@Test
public void shouldEqual() {
  double expected = 450.00d / (7d * 60);  // 1.0714285714285714
  double actual = 450.00d / 7d / 60;      // 1.0714285714285716

  assertThat(actual).isEqualTo(expected);
}
Run Code Online (Sandbox Code Playgroud)

但它失败了这个输出:

org.junit.ComparisonFailure: 
Expected :1.0714285714285714
Actual   :1.0714285714285716
Run Code Online (Sandbox Code Playgroud)

任何人都可以详细解释引擎盖下发生的事情导致1.000000000000000的价值X不同吗?

我在答案中寻找的一些要点是:精度丢失在哪里?首选哪种方法,为什么?哪个是正确的?(在纯数学中,两者都不对.也许两者都错了?)这些算术运算有更好的解决方案或方法吗?

Dav*_*d Z 42

我看到一堆问题告诉你如何解决这个问题,但没有一个真正解释发生了什么的问题,除了"浮点舍入误差是坏的,m'kay?" 那么让我来看看吧.我首先要指出的是,这个答案没有任何内容特定于Java.舍入误差是数字的任何固定精度表示所固有的问题,因此您在例如C中遇到相同的问题.

十进制数据类型中的舍入错误

作为一个简化的例子,假设我们有某种本机使用无符号十进制数据类型的计算机,我们可以调用它float6d.数据类型的长度为6位:4个专用于尾数,2个专用于指数.例如,数字3.142可以表示为

3.142 x 10^0
Run Code Online (Sandbox Code Playgroud)

它将以6位数字存储

503142
Run Code Online (Sandbox Code Playgroud)

前两位是指数加50,后四位是尾数.此数据类型可以从表示任何数量的0.001 x 10^-509.999 x 10^+49.

实际上,那不是真的.它不能存储任何数字.如果你想代表3.141592怎么办?还是3.1412034?还是3.141488906?幸运的是,数据类型不能存储超过四位数的精度,因此编译器必须对具有更多数字的任何内容进行舍入以适应数据类型的约束.如果你写

float6d x = 3.141592;
float6d y = 3.1412034;
float6d z = 3.141488906;
Run Code Online (Sandbox Code Playgroud)

然后编译器将这三个值中的每一个转换为相同的内部表示3.142 x 10^0(记住,存储为503142),这样x == y == z就会成立.

关键是有一整个实数范围都映射到相同的基础数字序列(或实际计算机中的位).具体而言,任何x令人满意的3.1415 <= x <= 3.1425(假设半偶数舍入)都被转换为表示503142以便存储在存储器中.

每次程序在内存中存储浮点值时都会发生这种舍入.它发生第一次是当你在源代码中编写一个常数,如我上面做了x,yz.每当您执行算术运算时,它会再次发生,这会增加超出数据类型所代表的精度位数.这些效果中的任何一个都称为舍入误差.有几种不同的方式可以发生:

  • 加法和减法:如果您添加的某个值与另一个值具有不同的指数,您将获得额外的精度数字,如果有足够数量,则需要删除最不重要的数字.例如,2.718和121.0都是可以在float6d数据类型中精确表示的值.但是如果你尝试将它们加在一起:

       1.210     x 10^2
    +  0.02718   x 10^2
    -------------------
       1.23718   x 10^2
    
    Run Code Online (Sandbox Code Playgroud)

    四舍五入到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
    
    Run Code Online (Sandbox Code Playgroud)

    四舍五入到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
    
    Run Code Online (Sandbox Code Playgroud)

    没有出现问题.

  • 师:这是事情变得凌乱的地方.除非你除以的数字碰巧是基数的幂(在这种情况下,除法只是数字移位,或二进制位移),除法几乎总会导致一些舍入误差.举一个例子,取两个非常简单的数字,3和7,除以它们,你得到

       3.                x 10^0
    /  7.                x 10^0
    ----------------------------
       0.428571428571... x 10^0
    
    Run Code Online (Sandbox Code Playgroud)

    最接近的值,以该数值可以被表示为float6d4.286 x 10^-1,或0.4286,这清楚地从确切的结果不同.

正如我们将在下一节中看到的那样,舍入引入的错误会随着您执行的每个操作而增加.因此,如果你正在使用"漂亮"的数字,就像在你的例子中一样,通常最好尽可能晚地进行除法运算,因为这些运算最有可能将舍入误差引入你之前不存在的程序中.

舍入误差分析

一般来说,如果你不能假设你的数字是"好的",那么舍入误差可以是正的也可以是负的,并且很难根据操作来预测它将走向哪个方向.这取决于所涉及的具体价值.查看该舍入误差的图2.718 z作为z(仍然使用float6d数据类型)的函数:

乘以的舍入误差为2.718

实际上,当您使用使用数据类型的完整精度的值时,通常更容易将舍入误差视为随机错误.查看该图,您可能会猜测误差的大小取决于操作结果的数量级.在这种特殊情况下,当z为10 -1的数量级时,2.718 z也是10 -1的数量级,因此它将是一个数量的形式0.XXXX.最大舍入误差是最后一位精度的一半; 在这种情况下,"精度的最后一位"是指0.0001,因此舍入误差在-0.00005和+0.00005之间变化.在2.718 z跳到下一个数量级(即1/2.718 = 0.3679)的点处,您可以看到舍入误差也会跳跃一个数量级.

您可以使用众所周知的错误分析技术来分析某个幅度的随机(或不可预测)错误如何影响您的结果.具体来说,对于乘法或除法,结果中的"平均"相对误差可以通过在每个操作数的正交中加上相对误差来近似- 也就是说,将它们平方,加上它们,然后取平方根.对于我们的float6d数据类型,相对误差在0.0005(对于像0.101这样的值)和0.00005(对于像0.995这样的值)之间变化.

0.1和1之间的相对误差

让我们将0.0001作为值x和相对误差的粗略平均值y.在相对误差x * yx / y由下式给出

sqrt(0.0001^2 + 0.0001^2) = 0.0001414
Run Code Online (Sandbox Code Playgroud)

这是一个sqrt(2)大于每个单独值的相对误差的因素.

在组合操作时,您可以多次应用此公式,每次浮点运算一次.因此,例如,对于z / (x * y),相对误差x * y平均为0.0001414(在此十进制示例中),然后相对误差z / (x * y)

sqrt(0.0001^2 + 0.0001414^2) = 0.0001732
Run Code Online (Sandbox Code Playgroud)

请注意,平均相对误差随着每个操作而增加,特别是作为乘法和除法的平方根.

类似地,因为z / x * y,平均相对误差z / x为0.0001414,相对误差z / x * y

sqrt(0.0001414^2 + 0.0001^2) = 0.0001732
Run Code Online (Sandbox Code Playgroud)

所以,同样,在这种情况下.这意味着对于任意值,平均而言,这两个表达式引入了大致相同的错误.(理论上,就是这样.我看到这些操作在实践中表现得非常不同,但这是另一个故事.)

血淋淋的细节

您可能对您在问题中提出的具体计算感到好奇,而不仅仅是平均值.对于那个分析,让我们切换到二进制算术的真实世界.大多数系统和语言中的浮点数使用IEEE标准754表示.对于64位数字,格式指定52位专用于尾数,11位指定指数,1位指示符号.换句话说,当写入基数2时,浮点数是表单的值

1.1100000000000000000000000000000000000000000000000000 x 2^00000000010
                       52 bits                             11 bits
Run Code Online (Sandbox Code Playgroud)

前导1未明确存储,并构成第53位.此外,您应该注意,存储以表示指数的11位实际上是实数指数加上1023.例如,此特定值为7,即1.75 x 2 2.尾数是二进制的1.75,或者1.11,二进制的指数是1023 + 2 = 1025,或者10000000001,因此存储在内存中的内容是

01000000000111100000000000000000000000000000000000000000000000000
 ^          ^
 exponent   mantissa
Run Code Online (Sandbox Code Playgroud)

但这并不重要.

你的例子也涉及450,

1.1100001000000000000000000000000000000000000000000000 x 2^00000001000
Run Code Online (Sandbox Code Playgroud)

和60,

1.1110000000000000000000000000000000000000000000000000 x 2^00000000101
Run Code Online (Sandbox Code Playgroud)

您可以使用此转换器或互联网上的任何其他值来使用这些值.

当你计算第一个表达式时450/(7*60),处理器首先进行乘法运算,得到420,或者

1.1010010000000000000000000000000000000000000000000000 x 2^00000001000
Run Code Online (Sandbox Code Playgroud)

然后它将450除以420.这产生15/14,即

1.0001001001001001001001001001001001001001001001001001001001001001001001...
Run Code Online (Sandbox Code Playgroud)

在二进制.现在,Java语言规范说明了这一点

不精确的结果必须四舍五入到最接近无限精确结果的可表示值; 如果两个最接近的可表示值相等,则选择具有最低有效位的那个值.这是IEEE 754标准的默认舍入模式,称为舍入到最近.

64位IEEE 754格式的最接近的可表示值为15/14

1.0001001001001001001001001001001001001001001001001001 x 2^00000000000
Run Code Online (Sandbox Code Playgroud)

大约1.0714285714285714是十进制的.(更确切地说,这是唯一指定此特定二进制表示的最不精确的十进制值.)

另一方面,如果先计算450/7,结果为64.2857142857 ...,或者是二进制,

1000000.01001001001001001001001001001001001001001001001001001001001001001...
Run Code Online (Sandbox Code Playgroud)

最近的可表示值是

1.0000000100100100100100100100100100100100100100100101 x 2^00000000110
Run Code Online (Sandbox Code Playgroud)

这是64.28571428571429180465 ...请注意由于舍入误差导致的二进制尾数的最后一位数(与精确值相比)的变化.将此除以60可以得到你

1.000100100100100100100100100100100100100100100100100110011001100110011...
Run Code Online (Sandbox Code Playgroud)

看结束:模式不同!这0011是重复,而不是001在其他情况下.最接近的可表示值是

1.0001001001001001001001001001001001001001001001001010 x 2^00000000000
Run Code Online (Sandbox Code Playgroud)

这与最后两位中的其他操作顺序不同:它们10不是01.十进制当量是1.0714285714285716.

如果您查看确切的二进制值,则应该清楚导致此差异的特定舍入:

1.0001001001001001001001001001001001001001001001001001001001001001001001...
1.0001001001001001001001001001001001001001001001001001100110011001100110...
                                                     ^ last bit of mantissa
Run Code Online (Sandbox Code Playgroud)

在这种情况下,前者的结果(数字15/14)恰好是精确值的最准确表示.这是一个如何离开分部直到最终使您受益的一个例子.但同样,只要您使用的值不使用数据类型的完整精度,此规则就会保留.一旦开始使用不精确(舍入)值,您就不再通过先进行乘法来保护自己免受进一步的舍入误差.


Emi*_*rey 5

它与如何实现double类型以及浮点类型与其他更简单的数字类型不能提供相同的精度保证这一事实有关.虽然以下答案更具体地说是总和,但它也通过解释如何在浮点数学运算中无法保证无限精度来回答您的问题:为什么更改总和顺序会返回不同的结果?.基本上,在不指定可接受的误差范围的情况下,您永远不应该尝试确定浮点值的相等性.Google的Guava库包括DoubleMath.fuzzyEquals(double, double, double)确定double一定精度内两个值的相等性.如果你想了解浮点相等的细节,这个网站是非常有用的 ; 同一站点也解释了浮点舍入错误.总之:由于操作顺序的计算之间的舍入不同,计算的预期值和实际值会有所不同.