c - 为什么这两个片段产生不同的值(value)?

标签 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

那么为什么它们以前不同而现在却相同呢?

最佳答案

首先,请更正您的代码。根据 C 标准,%lf 不是 *printf 的主要元素(“l”为 void,数据类型仍然为 double)。要打印 long double,应该使用 %Lf。使用您的变体 %lf,可能会遇到格式、截断值等不正确的错误。(您似乎运行 32 位环境:在 64 位中,Unix 和 Windows 都通过 double在 XMM 寄存器中,但 long double 在其他地方 - Unix 的堆栈,Windows 的指针内存。在 Windows/x86_64 上,您的代码将出现段错误,因为被调用者需要指针。但是,使用 Visual Studio,据我所知,long double 别名为 double,因此您可以对这一变化一无所知。)

其次,您无法确定此代码没有被您的 C 编译器针对编译时计算进行优化(这可以比默认运行时计算更精确)。为了避免这种优化,请将sum标记为 volatile 。

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

在 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

(与您的示例相同);

但是,在 FreeBSD 上使用 fpsetprec(FP_PE) 进行相同的测试,它将 FPU 切换为实长 double 运算:

4.47920533832942505776
4.47920533832942505820

5.17737751763962026144
5.17737751763962025971

与Linux情况相同;因此,在真实长 double 中,与 100 个被加数存在一些实际差异,并且根据常识,它比 50 个被加数大。但是您的平台默认四舍五入为双倍。

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

我现在还没有真正准备好调查 50 个被加数并四舍五入到双倍的结果来源,为什么它显示出如此巨大的差异以及为什么这种差异用 100 个被加数来补偿。这需要比我现在能承担的更深入的调查,但是,我希望这个答案清楚地向您展示了下一个需要挖掘的地方。

更新:如果是 Windows,您可以使用 _controlfp() 操作 FPU 模式。和 _controlfp_s().在 Linux 中,_FPU_SETCW 执行相同的操作。 This description详细阐述了一些细节并给出了示例代码。

更新2:使用Kahan summation在所有情况下都能给出稳定的结果。下面展示了4个值:i升序,无KS;升序 i,KS; i 降序,无 KS;降序 i,KS:

50 和 FPU 加倍:

4.47920533832942346919 4.47920533832942524555
4.47920533832942524555 4.47920533832942524555

100 和 FPU 加倍:

5.17737751763962084084 5.17737751763961995266
5.17737751763962084084 5.17737751763961995266

50 和 FPU 转为长 double :

4.47920533832942505776 4.47920533832942524555
4.47920533832942505820 4.47920533832942524555

100 和 FPU 转为长 double :

5.17737751763962026144 5.17737751763961995266
5.17737751763962025971 5.17737751763961995266

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

关于c - 为什么这两个片段产生不同的值(value)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33974176/

相关文章:

c++ - Long double 溢出但值小于最大可表示值

c - 如何反转链表的顺序?

c++ - 将字符串文字传递给函数不允许修改

c - C 中的十六进制浮点常量

c - 如何识别long double数据类型的精度

.net - 在 .NET 中转换 long double

c - 在没有人等待时发出条件变量信号的意义?

c - 截图程序无法运行

ios - Swift:将 String 转换为 Float 并在进行一些数学运算后再次返回 String

python - 如何使用 Pandas 子选择与 float dtype 相等的行