【问题标题】:Is floating-point addition and multiplication associative?浮点加法和乘法是否关联?
【发布时间】:2012-05-09 10:54:39
【问题描述】:

我在添加三个浮点值并将它们与 1 进行比较时遇到问题。

cout << ((0.7 + 0.2 + 0.1)==1)<<endl;     //output is 0
cout << ((0.7 + 0.1 + 0.2)==1)<<endl;     //output is 1

为什么这些值会不同?

【问题讨论】:

  • 您的示例代码在 commutativity 而非 associativity 方面有所不同。展示关联性的版本是 (0.7 + (0.1 + 0.2))
  • @MattMcNabb: + 是二元运算。对于浮点操作数,它是可交换的,但不是关联的。因此,如果您有两个产生不同结果的表达式,则不能仅通过应用交换性来形成另一个表达式。
  • @tmyklebu 好的,所以当且仅当已知交换性成立时,它才会检查关联性。 (C++ 标准似乎不保证可交换性)。

标签: c++ floating-point multiplication addition associativity


【解决方案1】:

浮点加法不一定是关联的。如果您更改添加的顺序,这可能会改变结果。

该主题的标准论文是What Every Computer Scientist Should Know about Floating Point Arithmetic。它给出了以下示例:

另一个灰色区域涉及括号的解释。由于舍入误差,代数的结合定律不一定适用于浮点数。例如,当 x = 1e30、y = -1e30 和 z = 1 时,表达式 (x+y)+z 与 x+(y+z) 的答案完全不同(前者为 1,后者为 0 )。

【讨论】:

    【解决方案2】:

    对于当前流行的机器和软件,可能的情况是:

    编译器将 .7 编码为 0x1.6666666666666p-1(这是十六进制数字 1.6666666666666 乘以 2 的 -1 次幂),.2 编码为 0x1.999999999999ap-3,@9817654323@ 为 0.x 999999999999ap-4。其中每一个都是可以用浮点数表示的最接近您编写的十进制数字的数字。

    请注意,这些十六进制浮点常量中的每一个都在其有效数字中恰好有 53 位(“小数”部分,通常不准确地称为尾数)。有效数字的十六进制数字有一个“1”和另外 13 个十六进制数字(每个 4 位,总共 52 个,包括“1”),这是 IEEE-754 标准为 64 位二进制浮点提供的 -点数。

    让我们将 .7.2 的数字相加:0x1.6666666666666p-1 和 0x1.999999999999ap-3。首先,缩放第二个数字的指数以匹配第一个。为此,我们将指数乘以 4(将“p-3”更改为“p-1”)并将有效位乘以 1/4,得到 0x0.66666666666668p-1。然后添加 0x1.6666666666666p-1 和 0x0.66666666666668p-1,得到 0x1.ccccccccccccc8p-1。请注意,该数字的有效位超过 53 位:“8”是句点后的第 14 位。浮点数不能返回这么多位的结果,所以它必须四舍五入到最接近的可表示数字。在这种情况下,有两个相同的数字,0x1.cccccccccccccp-1 和 0x1.ccccccccccccdp-1。当出现平局时,使用有效数字最低位为零的数字。 “c”是偶数,“d”是奇数,所以使用“c”。添加的最终结果是0x1.cccccccccccccp-1。

    接下来,将.1 (0x1.999999999999ap-4) 的号码添加到其中。同样,我们缩放以使指数匹配,因此 0x1.999999999999ap-4 变为 0x.33333333333334p-1。然后将其添加到 0x1.cccccccccccccp-1,得到 0x1.fffffffffffff4p-1。将其四舍五入为 0x1.fffffffffffffp-1,这就是 .7+.2+.1 的最终结果。

    现在考虑.7+.1+.2。对于.7+.1,添加 0x1.6666666666666p-1 和 0x1.999999999999ap-4。回想一下,后者被缩放到 0x.33333333333334p-1。那么确切的总和是 0x1.99999999999994p-1。将其四舍五入为 53 位得到 0x1.9999999999999p-1。

    然后添加.2 (0x1.999999999999ap-3) 的数字,缩放为 0x0.66666666666668p-1。确切的总和是 0x2.00000000000008p-1。浮点有效数总是从 1 开始缩放(除了特殊情况:零、无穷大和可表示范围底部的非常小的数字),因此我们将其调整为 0x1.00000000000004p0。最后,我们四舍五入到 53 位,得到 0x1.0000000000000p0。

    因此,由于舍入时出现错误,.7+.2+.1 返回 0x1.ffffffffffffffp-1(略小于 1),.7+.1+.2 返回 0x1.0000000000000p0(正好是 1)。

    【讨论】:

    • @Evg:我在文本中的数字是数学数字,不是源代码或浮点数。不应将它们放入源代码样式中。我用引号写的各种东西,例如“.7+.1+.2”,确实代表了使用浮点算术进行的计算,并且可以以源代码格式而不是引号显示,所以我会考虑将其批准为编辑。
    • @Evg:将一些“计算”出来的数字(而不是精确的数学数字)放在源代码样式中并不是一个坏主意,所以我会看看。跨度>
    【解决方案3】:

    浮点乘法在 C 或 C++ 中不具有关联性。

    证明:

    #include<stdio.h>
    #include<time.h>
    #include<stdlib.h>
    using namespace std;
    int main() {
        int counter = 0;
        srand(time(NULL));
        while(counter++ < 10){
            float a = rand() / 100000;
            float b = rand() / 100000;
            float c = rand() / 100000;
    
            if (a*(b*c) != (a*b)*c){
                printf("Not equal\n");
            }
        }
        printf("DONE");
        return 0;
    }
    

    在这个程序中,大约 30% 的时间,(a*b)*c 不等于 a*(b*c)

    【讨论】:

    • 如果RAND_MAX &lt; 100000,则为0%!
    【解决方案4】:

    加法和乘法都不与 IEEE 743 双精度(64 位)数字相关联。以下是每个示例(使用 Python 3.9.7 评估):

    >>> (.1 + .2) + .3
    0.6000000000000001
    >>> .1 + (.2 + .3)
    0.6
    
    >>> (.1 * .2) * .3
    0.006000000000000001
    >>> .1 * (.2 * .3)
    0.006
    

    【讨论】:

      【解决方案5】:

      与 Eric 的答案类似,但需要添加,并且使用 Python。

      import random
      
      random.seed(0)
      n = 1000
      a = [random.random() for i in range(n)]
      b = [random.random() for i in range(n)]
      c = [random.random() for i in range(n)]
      
      sum(1 if (a[i] + b[i]) + c[i] != a[i] + (b[i] + c[i]) else 0 for i in range(n))
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-01-14
        • 1970-01-01
        • 1970-01-01
        • 2011-05-06
        • 2014-02-14
        • 2023-03-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多