为什么两个看似相同的浮点数乘以它们的倒数时会有不同的行为?

时间:2016-10-11 20:58:07

标签: floating-point numerical floating-point-conversion

在将我链接到 Is floating point math broken?之前 - 请先阅读问题。我知道这是如何工作的。这个问题特定于我发现的两个数字(好吧,可能存在更多这样的对,但我希望这个特定的行为得到解释)。

我有两个浮点常量:1.91.1。使用Python,我将这两者乘以它们的倒数并得到以下结果:

>>> x = 1.1
>>> y = 1.9
>>> print("%.55f" % x)
1.1000000000000000888178419700125232338905334472656250000
>>> print("%.55f" % y)
1.8999999999999999111821580299874767661094665527343750000
>>> print("%.55f" % (1/x))
0.9090909090909090606302811465866398066282272338867187500
>>> print("%.55f" % (1/y))
0.5263157894736841813099204046011436730623245239257812500
>>> print("%.55f" % ((1/x) * x))
1.0000000000000000000000000000000000000000000000000000000
>>> print("%.55f" % ((1/y) * y))
0.9999999999999998889776975374843459576368331909179687500

我当然知道在硬件中实现浮点运算的问题,但我无法理解为什么这些数字显示出这种不同的行为。考虑他们的倒数;它们的确切结果如下(使用WolframAlpha计算,为简洁起见省略了后面的数字):

  • 1/x0.909090909090909017505915727262383419481191148103102677263...
  • 1/y0.526315789473684235129596113576877392185605155259237553584...

显然,我的硬件计算的倒数是正确的,直到两个数字的大约15-16位十进制数字;没有区别。还有什么可能会有所不同?如果(1/x) * x 完全 1,最终没有任何“垃圾”怎么可能?

为了完整起见,我或许应该提到我在x86 PC上,所以这是所有64位IEEE 754算法:

>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

1 个答案:

答案 0 :(得分:4)

我在这个答案的最后写了一个Java程序,它显示了正在发生的事情。我使用Java是因为BigDecimal提供了一种方便的方法来进行精确的打印输出和简单的计算。

IEEE浮点运算就好像计算机首先计算出确切的结果,然后将其舍入到最接近的可表示值。使用BigDecimal,我可以显示确切的产品,然后显示上下舍入的结果。

我为每个输入做了这个,当产品正好是1.0时,我也用2.0来测试程序。

以下是x的输出:

x=1.100000000000000088817841970012523233890533447265625
1/x=0.90909090909090906063028114658663980662822723388671875
Exact product = 1.00000000000000004743680196125668585607481256497662217592534562686512611406897121923975646495819091796875
Round down = 1
Round down error = 4.743680196125668585607481256497662217592534562686512611406897121923975646495819091796875E-17
Round up = 1.0000000000000002220446049250313080847263336181640625
Round up error = 1.7460780296377462222865152105318744032407465437313487388593102878076024353504180908203125E-16

确切的答案用1.0括起来,稍微大一点,但1.0是更近的,因此乘法的舍入到最接近的结果。

y=1.899999999999999911182158029987476766109466552734375
1/y=0.52631578947368418130992040460114367306232452392578125
Exact product = 0.99999999999999989774261615294611071381321879759485743989659632495470287238958917441777884960174560546875
Round down = 0.99999999999999988897769753748434595763683319091796875
Round down error = 8.76491861546176475617638560667688868989659632495470287238958917441777884960174560546875E-18
Round up = 1
Round up error = 1.0225738384705388928618678120240514256010340367504529712761041082558222115039825439453125E-16

在这种情况下,确切的产品被括号1.0和略小的东西。较小的值更接近确切的结果。

最后:

Exact case=2
1/Exact case=0.5
Exact product = 1.0

在两个测试用例中,确切的产品不能用Java double,IEEE 754 64位二进制浮点表示。结果是最接近确切产品的结果。不同之处在于精确产品与1.0的接近程度以及与其他括号可表示的数字的接近程度。

以下是该计划:

import java.math.BigDecimal;

public class Test {
  public static void main(String[] args) {
    testit(1.1, "x");
    testit(1.9, "y");
    testit(2.0, "Exact case");
  }

  private static void testit(double val, String name) {
    BigDecimal valBD = new BigDecimal(val);
    System.out.println(name + "=" + valBD);
    double inv = 1 / val;
    BigDecimal invBD = new BigDecimal(inv);
    System.out.println("1/" + name + "=" + invBD);
    BigDecimal exactProduct = valBD.multiply(invBD);
    System.out.println("Exact product = " + exactProduct);
    double rawRound = exactProduct.doubleValue();
    BigDecimal rawRoundBD = new BigDecimal(rawRound);
    int comp = rawRoundBD.compareTo(exactProduct);
    double down = 0;
    BigDecimal downBD;
    double up = 0;
    BigDecimal upBD;
    if (comp == 0) {
      return;
    } else if (comp < 0) {
      down = rawRound;
      up = Math.nextUp(down);
    } else {
      up = rawRound;
      down = Math.nextDown(up);
    }
    downBD = new BigDecimal(down);
    upBD = new BigDecimal(up);
    BigDecimal downError = exactProduct.subtract(downBD);
    BigDecimal upError = upBD.subtract(exactProduct);
    System.out.println("Round down = " + downBD);
    System.out.println("Round down error = " + downError);
    System.out.println("Round up = " + upBD);
    System.out.println("Round up error = " + upError);
    System.out.println();

  }

}
相关问题