【问题标题】:Why do two seemingly identical floating point numbers have different behavior when multiplied by their reciprocals?为什么两个看似相同的浮点数乘以它们的倒数时会有不同的行为?
【发布时间】:2016-10-12 07:01:20
【问题描述】:

在链接我之前 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/x: 0.909090909090909017505915727262383419481191148103102677263...
  • 1/y: 0.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)

【问题讨论】:

  • 我理解您的反对意见,但首先:我已阅读您的问题 :) 您可能还应该添加一个标签来指定您使用的编程语言(可能是 python-3.something ?)。
  • 但是您想知道为什么 (1/z)*z 对于不同的 z 是不同的这一事实证明您没有完全理解浮点数,抱歉。跨度>
  • @MarcusMüller 事实上,我还没有完全理解这个主题——否则就没有问题要问了!但我认为,某个特定问题已在 SO 的其他地方部分涵盖这一事实并不意味着与之相关的新问题以及无法开始询问其他问题。
  • 任何略小于一和一的数和略大于一的数都是(1/z)*z的可能结果,这正是有限长度算术的本质。所以你可能会随意选择所有符合 IEEE754 的硬件和软件实现(没有单一的“正确”方式),并尝试直到你得到准确的结果。因为我不知道你使用的是哪个 FPU/CPU,或者你的脚本语言使用了哪些浮点指令并且没有参与其中任何一个的实现,“它是特定于实现的”是我能做的最好的

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


【解决方案1】:

我在这个答案的末尾写了一个 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();

  }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-26
    • 1970-01-01
    相关资源
    最近更新 更多