当我运行以下代码时:
#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
那么为什么他们现在和现在不同呢?
答案 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
你可以看到差异消失,结果稳定。我认为这几乎是最后一点,可以在这里添加:)