为什么这两个片段会产生不同的价值?

时间:2015-11-28 17:22:25

标签: c floating-point long-double

当我运行以下代码时:

#include <stdio.h>

int main()
{
    int i = 0;
    volatile long double sum = 0;
    for (i = 1; i < 50; ++i) /* first snippet */
    {
        sum += (long double)1 / i;
    }
    printf("%.20Lf\n", sum);
    sum = 0;
    for (i = 49; i > 0; --i) /* second snippet */
    {
        sum += (long double)1 / i;
    }
    printf("%.20Lf", sum);
    return 0;
}

输出结果为:

4.47920533832942346919
4.47920533832942524555

这两个号码不一样吗? 更有趣的是,以下代码:

#include <stdio.h>

int main()
{
    int i = 0;
    volatile long double sum = 0;
    for (i = 1; i < 100; ++i) /* first snippet */
    {
        sum += (long double)1 / i;
    }
    printf("%.20Lf\n", sum);
    sum = 0;
    for (i = 99; i > 0; --i) /* second snippet */
    {
        sum += (long double)1 / i;
    }
    printf("%.20Lf", sum);
    return 0;
}

产生

5.17737751763962084084
5.17737751763962084084

那么为什么他们现在和现在不同呢?

1 个答案:

答案 0 :(得分:2)

首先,请更正您的代码。按C标准,%lf不是* printf的主体('l'为void,数据类型保持双精度)。要打印长双,应使用%Lf。使用您的变体%lf,可能会出现格式不正确,值减少等错误。(您似乎在运行32位环境:在64位中,Unix和Windows在XMM寄存器中都传递了两倍,但是长的其他地方 - 用于Unix的堆栈,用于Windows的指针的内存。在Windows / x86_64上,你的代码将是段错误的,因为被调用者需要指针。但是,使用Visual Studio,long double是AFAIK别名加倍,所以你可以保持对此无知变化。)

其次,您不能确定C编译器没有优化此代码进行编译时计算(可以比默认运行时更精确地完成)。要避免此类优化,请将sum标记为易失性。

通过这些更改,您的代码会显示:

在Linux / amd64,gcc4.8:

表示50:

4.47920533832942505776
4.47920533832942505820

表示100:

5.17737751763962026144
5.17737751763962025971

在FreeBSD / i386,gcc4.8,没有精度设置或使用显式fpsetprec(FP_PD):

4.47920533832942346919
4.47920533832942524555

5.17737751763962084084
5.17737751763962084084

(与您的示例相同);

但是,使用fpsetprec(FP_PE)在FreeBSD上进行相同的测试,它将FPU切换为真正的长双操作:

4.47920533832942505776
4.47920533832942505820

5.17737751763962026144
5.17737751763962025971

与Linux案例相同;所以,在 real long double中,100个加法有一些真正的区别,根据常识,它大于50。但是你的平台默认舍入为double。

最后,总的来说,这是众所周知的有限精度和随后的舍入效应。例如,在this classical book中,在第一章中解释了减少数字序列和的这种错误。

我现在还没准备好用50个加法来调查结果的来源并且舍入到加倍,为什么它显示出如此巨大的差异以及为什么这个差异用100个加法补偿。这需要比我现在能够负担得起的更深入的调查,但是,我希望,这个答案清楚地告诉你下一个挖掘的地方。

更新:如果是Windows,您可以使用_controlfp()_controlfp_s().操纵FPU模式在Linux中,_FPU_SETCW也是如此。 This description详细说明了一些细节并提供了示例代码。

UPDATE2:使用Kahan summation可以在所有情况下获得稳定的结果。以下显示4个值:升序i,无KS;提升我,KS;降临我,没有KS;降临我,KS:

50和FPU加倍:

4.47920533832942346919 4.47920533832942524555
4.47920533832942524555 4.47920533832942524555

100和FPU加倍:

5.17737751763962084084 5.17737751763961995266
5.17737751763962084084 5.17737751763961995266

50和FPU加倍:

4.47920533832942505776 4.47920533832942524555
4.47920533832942505820 4.47920533832942524555

100和FPU加倍:

5.17737751763962026144 5.17737751763961995266
5.17737751763962025971 5.17737751763961995266

你可以看到差异消失,结果稳定。我认为这几乎是最后一点,可以在这里添加:)