为什么这两个代码片段会产生不同的价值? [英] Why these two snippets generate different value?

查看:86
本文介绍了为什么这两个代码片段会产生不同的价值?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我运行以下代码时:

#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

两个数字不应该相同吗? 更有趣的是,以下代码:

Shouldn't the two numbers be same? And more interestingly, the following code:

#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

那么为什么它们现在又不同了?

So why are they different then and same now?

推荐答案

首先,请更正您的代码.按照C标准,%lf不是* printf的主体('l'为空,数据类型保持为double).要打印长整数,应使用%Lf.使用变体%lf,可能会遇到格式不正确,缩减值等错误.(您似乎在32位环境下运行:在64位环境下,Unix和Windows在XMM寄存器中都传递了两倍,但是很长在Windows/x86_64上,由于callee期望使用指针,因此在Windows/x86_64上,代码将进行段错误处理,但是对于Visual Studio,long double是AFAIK的别名,因此,您仍然可以忽略此更改. )

First, please correct your code. By C standard, %lf isn't principal for *printf ('l' is void, the data type remains double). To print long double, one should use %Lf. With your variant %lf, it's possible to get into a bug with improper format, cut-down value, etc. (You seem running 32-bit environment: in 64 bits, both Unix and Windows pass double in XMM registers, but long double otherwhere - stack for Unix, memory by pointer for Windows. On Windows/x86_64, you code will segfault because callee expects pointer. But, with Visual Studio, long double is AFAIK aliased to double, so you can remain ignorant of this change.)

第二,您不能确定C编译器未将此代码优化为编译时计算(比默认运行时更精确).为避免这种优化,请将sum标记为易失性.

Second, you can't be sure this code is not optimized by your C compiler to compile-time calculations (which can be done with more precision than default run-time one). To avoid such optimization, mark sum as volatile.

进行这些更改后,您的代码将显示:

With these changes, your code shows:

在Linux/amd64,gcc4.8:

At Linux/amd64, gcc4.8:

持续50:

4.47920533832942505776
4.47920533832942505820

对于100:

5.17737751763962026144
5.17737751763962025971

在FreeBSD/i386上为gcc4.8,未进行精确设置或使用显式fpsetprec(FP_PD):

At FreeBSD/i386, gcc4.8, without precision setting or with explicit fpsetprec(FP_PD):

4.47920533832942346919
4.47920533832942524555

5.17737751763962084084
5.17737751763962084084

(与您的示例相同);

(the same as in your example);

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

but, the same test on FreeBSD with fpsetprec(FP_PE), which switches FPU to real long double operations:

4.47920533832942505776
4.47920533832942505820

5.17737751763962026144
5.17737751763962025971

与Linux情况相同;因此,在 real 长整数中,有100个被加数有一些实际差异,按照常识,它大于50个.但是您的平台默认将舍入为双倍.

identical to Linux case; so, in real long double, there is some real difference with 100 summands, and it is, in accordance with common sense, larger than for 50. But your platform defaults to rounding to double.

最后,总的来说,这是有限精度和随之而来的四舍五入的众所周知的效果.例如,在这本经典书中,在第一章中解释了递减数序列之和的舍入.

And, finally, in general, this is well-known effect of a finite precision and consequent rounding. For example, in this classical book, this misrounding of decreasing number series sum is explained in the very first chapters.

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

I am not really ready now to investigate source of results with 50 summands and rounding to double, why it shows such huge difference and why this difference is compensated with 100 summands. That needs much deeper investigation than I can afford now, but, I hope, this answer clearly shows you a next place to dig.

更新:如果是Windows,则可以使用 _controlfp_s().在在Linux上,_FPU_SETCW执行相同的操作. 此描述详细说明了一些细节并给出了示例代码.

UPDATE: if it's Windows, you can manipulate FPU mode with _controlfp() and _controlfp_s(). In Linux, _FPU_SETCW does the same. This description elaborates some details and gives example code.

UPDATE2:使用 Kahan求和可以在所有情况下提供稳定的结果.以下显示了4个值:升序i,无KS;升上我,KS;降i,无KS;降序i,KS:

UPDATE2: using Kahan summation gives stable results in all cases. The following shows 4 values: ascending i, no KS; ascending i, KS; descending i, no KS; descending i, KS:

50和FPU加倍:

4.47920533832942346919 4.47920533832942524555
4.47920533832942524555 4.47920533832942524555

100和FPU加倍:

100 and FPU to double:

5.17737751763962084084 5.17737751763961995266
5.17737751763962084084 5.17737751763961995266

50和FPU加倍:

4.47920533832942505776 4.47920533832942524555
4.47920533832942505820 4.47920533832942524555

100和FPU加倍:

100 and FPU to long double:

5.17737751763962026144 5.17737751763961995266
5.17737751763962025971 5.17737751763961995266

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

you can see difference disappeared, results are stable. I would assume this is nearly final point that can be added here :)

这篇关于为什么这两个代码片段会产生不同的价值?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆