浮点数加法和乘法是否满足结合律?

22

当我将三个浮点数相加并将它们与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

为什么这些值会有不同的输出?


1
你的示例代码在可交换性方面有所不同,而不是结合性。一个演示结合性的版本将是(0.7 + (0.1 + 0.2)) - M.M
2
@MattMcNabb:+ 是一种二元操作。对于浮点数操作数,它是可交换的但不是结合的。因此,如果您有两个产生不同结果的表达式,您不能仅通过应用可交换性从一个表达式得到另一个表达式。 - tmyklebu
1
@tmyklebu 好的,所以只有在已知交换律成立的情况下,这确实会检查结合性。(C++标准似乎并不保证交换律)。 - M.M
5个回答

36

浮点数加法不一定是结合律的。如果您改变相加顺序,结果可能会发生改变。

该主题的标准文献为《计算机科学家应知道的浮点运算》。它提供了以下示例:

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


10
当前流行的机器和软件,最可能的情况是:
编译器将.7编码为0x1.6666666666666p-1(十六进制数1.6666666666666乘以2的-1次方),.2编码为0x1.999999999999ap-3,.1编码为0x1.999999999999ap-4。其中每个都是浮点数中可表示的最接近你输入的十进制数值的数字。
请注意,这些十六进制浮点常量的尾数(通常不准确地称为小数部分)恰好有53位。尾数的十六进制数有一个“1”和十三个更多的十六进制数字(每个四位,总共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。将其舍入到53位,得到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.fffffffffffffp-1(略小于1),.7+.1+.2返回0x1.0000000000000p0(恰好是1)。

@Evg:我在文本中的数字是数学数字,而不是源代码或浮点数。它们不应该被放入源代码样式中。我在引号中写的各种东西,比如“.7+.1+.2”,代表使用浮点算术进行的计算,可以显示为源代码格式,而不是引号,因此我会考虑批准这种编辑。 - Eric Postpischil
@Evg:把一些“计算出来的”数字以源代码风格呈现也不是个坏主意,我会考虑一下这个建议。 - Eric Postpischil

5
浮点数乘法在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)

6
如果 RAND_MAX < 100000,则有0%的几率。 - M.M
@M.M 这个怎么可以正式证明呢? - pmor
如果 RAND_MAX < 100000,那么 rand() / 100000 == 0,所以 != 测试永远不会失败。 - M.M
@M.M 确实。我曾经看到过 / 被用作 %(通常与 rand() 一起使用,这是 BTW "poor" per comp.lang.c FAQ list); - pmor

1

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

0

和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))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接