0

我正在尝试使用 epsilon 比较具有双精度的值。但是,我有一个问题 - 最初我认为差异应该等于 epsilon,但事实并非如此。此外,当我尝试使用连续乘法检查二进制表示时,发生了一些奇怪的事情,我感到很困惑,因此我很感激你对问题的解释和对我的思维方式的评论

#include <stdio.h>

#define EPSILON 1e-10

void double_equal(double a, double b) {
    printf("a: %.12f, b: %.12f, a - b = %.12f\n", a, b, a - b);
    printf("a: %.12f, b: %.12f, b - a = %.12f\n", a, b, b - a);
    if (a - b < EPSILON) printf("a - b < EPSILON\n");
    if (a - b == EPSILON) printf("a - b == EPSILON\n");
    if (a - b <= EPSILON) printf("a - b <= EPSILON\n");
    if (b - a <= EPSILON) printf("b - a <= EPSILON\n");
}

int main(void) {
    double wit1 = 1.0000000001;
    double wit2 = 1.0;
    double_equal(wit1, wit2);
    return 0;
}

输出是:

a: 1.000000000100, b: 1.000000000000, a - b = 0.000000000100
a: 1.000000000100, b: 1.000000000000, b - a = -0.000000000100
b - a <= EPSILON

如果我们不在数字 ( #define EPSILON 1e-10F) 之后提供“F”/“f”符号,则 C 中的数字常量被声明为双精度数,因此我在这里看不到这个问题中的转换问题。因此,我为这些特定示例创建了非常简单的程序(我知道它应该包括处理将整数部分转换为二进制数)。

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

char* convert(double a) {
    char* res = malloc(200);
    int count = 0;
    double integral;
    a = modf(a, &integral);

    if (integral == 1) {
        res[count++] = integral + '0';
        res[count++] = '.';
    } else {
        res[count++] = '0';
        res[count++] = '.';
    }

    while(a != 0 && count < 200) {
        printf("%.100f\n", a);
        a *= 2;
        a = modf(a, &integral);
        if (integral == 1) res[count++] = integral + '0';
        else res[count++] = '0';
    }

    res[count] = '\0';
    return res;
}

int main(void) {
    double wit1 = 1.0000000001;
    double diff = 0.0000000001;
    char* res = convert(wit1);
    char* di = convert(diff);
    printf("this: %s\n", res);
    printf("diff: %s\n", di);
    return 0;
}

直接输出:

this: 1.0000000000000000000000000000000001101101111100111
diff: 0.00000000000000000000000000000000011011011111001101111111011001110101111011110110111011

第一个问题:为什么差异中有这么多结尾的零?为什么二进制点后的结果不同?

但是,如果我们查看计算过程和小数部分,打印出来(我只展示前几行):

1.0000000001:
0.0000000001000000082740370999090373516082763671875000000000000000000000000000000000000000000000000000
0.0000000002000000165480741998180747032165527343750000000000000000000000000000000000000000000000000000
0.0000000004000000330961483996361494064331054687500000000000000000000000000000000000000000000000000000

0.0000000001:
0.0000000001000000000000000036432197315497741579165547065599639608990401029586791992187500000000000000
0.0000000002000000000000000072864394630995483158331094131199279217980802059173583984375000000000000000
0.0000000004000000000000000145728789261990966316662188262398558435961604118347167968750000000000000000

第二个问题:为什么会有这么多奇怪的结尾数字?这是由于无法精确表示十进制值的浮点运算的结果吗?

分析减法,我可以看到为什么结果大于 epsilon。我遵循以下程序:

  1. 为要减去的序列准备一个零一的补序列
  2. “添加”序列
  3. 减去开头的那个,添加到最右边的位

所以:

   1.0000000000000000000000000000000001101101111100111
 - 1.0000000000000000000000000000000000000000000000000
               |
              \/
   1.0000000000000000000000000000000001101101111100111
"+"0.1111111111111111111111111111111111111111111111111    
 --------------------------------------------------------
  10.0000000000000000000000000000000001101101111100110    
          |
          \/
   0.0000000000000000000000000000000001101101111100111

与 的计算值比较epsilon

0.000000000000000000000000000000000110110111110011 0 1111111011001110101111011110110111011
0.000000000000000000000000000000000110110111110011 1

空格表示差异。

第三个问题:如果我不能比较等于epsilon的值,我是否需要担心?我认为这种情况表明了与 epsilon 的公差间隔是为了什么而制定的。但是,有什么我应该改变的吗?

4

2 回答 2

1

此答案假定您的 C 实现使用 IEEE-754 binary64,也称为其double类型的“双”格式。这很常见。

如果 C 实现正确舍入,则double wit1 = 1.0000000001;初始化wit1为 1.0000000001000000082740370999090373516082763671875。This is because the two representable values nearest 1.0000000001 are 1.000000000099999786229432174877729266881942749023437500000000000000000000000000000000000000000000000 and 1.0000000001000000082740370999090373516082763671875. 选择后者是因为它更接近。

如果正确舍入,1e-10用于EPSILON将产生 0.000000000100000000000000003643219731549774157916554706559963960899040102958679199218750000000000000。

明显wit1 - 1超过EPSILON,因此 中的测试a - b < EPSILON评估double_equal为假。

第一个问题:为什么差异中有这么多结尾的零?

计算从第一个 1 到最后一个 1 的位数。在每个数字中,有 53 位。那是因为 a 的有效位有 53 位double。您的数字恰好以 1 位结尾,这有点巧合。大约一半的时间,尾随位为 0,四分之一的时间,最后两位为零,依此类推。但是,由于 a 的有效位中有 53 位double,因此从第一个 1 位到作为表示值的一部分的最后一位将恰好有 53 位。

由于您的第一个数字在整数位置以 1 开头,因此之后最多有 52 位。此时,数字必须四舍五入到最接近的可表示值。

由于您的第二个数字介于 2 -34和 2 -33之间,因此它的第一个 1 位位于 2 -34位置,并且它可以在必须四舍五入之前到达 2 -86位置。

第三个问题:如果我不能比较等于epsilon的值,我是否需要担心?

为什么要与 epsilon 进行比较?没有用于比较包含先前操作错误的浮点数的通用解决方案。是否可以或应该使用“epsilon 比较”取决于应用程序以及所涉及的操作和数量。

于 2021-08-27T23:41:41.427 回答
1

为什么二进制点后的结果不同?

因为这就是区别。

期待其他东西来自思考1.00000000010.0000000001double两个价值观也是如此。他们不。它们的差异不是 1.0。它们的值接近这两个值,每个值都有大约 53 个二进制数字。他们的差距接近于最后一位的单位1.0000000001

为什么会有这么多奇怪的结尾数字?这是由于无法精确表示十进制值的浮点运算的结果吗?

有些。
double可以编码大约 2 64 个不同的数字。 1.0000000001并且0.0000000001不在那个集合中。取而代之的是附近的那些看起来像奇怪的结束数字

如果我不能比较等于 epsilon 的值,我是否需要担心?我认为这种情况表明了与 epsilon 的公差间隔是为了什么而制定的。但是,有什么我应该改变的吗?

是的,改变使用epsilon. epsilon对相对差异有用,而不是绝对差异。非常大的连续double值相距epsilon甚远。大约 45% 的double,(小的)都小于epsilon数量级。即使它们在数量级上相差数万亿倍,对于小型来说,if (a - b <= EPSILON) printf("a - b <= EPSILON\n");要么是正确的,要么if (b - a <= EPSILON) printf("b - a <= EPSILON\n");将是正确的。a, b

简单化:

if (fabs(a-b) < EPSILON*fabs(a + b)) {
  return values_a_b_are_near_each_other;
}
于 2021-08-27T23:31:43.500 回答