【问题标题】:Different Answers by removing a printf statement通过删除 printf 语句得到不同的答案
【发布时间】:2015-06-14 03:16:19
【问题描述】:

这是UVa在线法官问题的链接。
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=29&page=show_problem&problem=1078

我的 C 代码是

#include <stdio.h>

double avg(double * arr,int students)
{

    int i;
    double average=0;

    for(i=0;i<students;i++){
        average=average+(*(arr+i));
    }

    average=average/students;

    int temp=average*100;

    average=temp/100.0;

    return average;
}


double mon(double * arr,int students,double average)
{
    int i;
    double count=0;

    for(i=0;i<students;i++){

        if(*(arr+i)<average){
            double temp=average-*(arr+i);

            int a=temp*100;

            temp=a/100.0;

            count=count+temp;
        }
    }

    return count;
}


int main(void)
{
    // your code goes here
    int students;

    scanf("%d",&students);

    while(students!=0){

        double arr[students];
        int i;

        for(i=0;i<students;i++){
            scanf("%lf",&arr[i]);

        }

        double average=avg(arr,students);

        //printf("%lf\n",average);

        double money=mon(arr,students,average);

        printf("$%.2lf\n",money);

        scanf("%d",&students);

    }

    return 0;
}

输入和输出之一是
输入
3
0.01
0.03
0.03
0

输出
$0.01

我的输出是
0.00 美元。

但是,如果我取消注释 printf("%lf",average);

输出如下
0.02 //这是平均值
0.01 美元

我在 ideone.com 上运行代码

请解释为什么会这样。

【问题讨论】:

  • (*(arr+i))而不是arr[i]不会让你看起来更酷:)
  • 这可能是某种舍入问题 - 也许您的计算机正在将输出计算为 0.00999999 或其他东西。如果是这样,你可能想用整数美分来做所有的数学运算(假设问题允许你这样做)。
  • ...但是printf 实际上确实对输出进行了舍入,所以不可能这样。我也得到$0.01 作为输出,奇怪的是......
  • @AdityaSharma:$0.01 在我的机器上,$0.00 on IDEOne。我还没有在这里看到任何 UB 的东西,但有些东西显然是可疑的......
  • 请注意,顺便说一句,可变长度数组是 C99 的一项功能,尽管 GCC 在 C90 模式下也默认识别它们。您的代码可以接受地使用它们,但它确实会在一个有点靠近边缘的区域使用它更新 VLA 范围内的长度表达式的值的方式,从而在每次迭代时为 VLA 提供不同的长度。这些不是编程错误,但它们可能存在编译器错误的地方。

标签: c


【解决方案1】:

我相信我已经找到了罪魁祸首和合理的解释。

在 x86 处理器上,FPU 在内部以 扩展精度 运行,这是一种 80 位格式。所有浮点指令都以这种精度运行。如果实际需要double,编译器将生成代码以将扩展精度值向下转换为双精度值。至关重要的是,注释掉的 printf 强制进行这种转换,因为 FPU 寄存器必须在该函数调用中保存和恢复,并且它们将保存为 doubles(请注意,avgmon 都是内联的所以那里没有保存/恢复)。

事实上,我们可以使用static double dummy = average;这一行代替printf来强制发生double转换,这也会导致bug消失:http://ideone.com/a1wadn

由于浮点数不准确,您的 average 值接近但不完全是 0.02。当我使用long double 显式进行所有计算,并打印出average 的值时,如下所示:

long double: 0.01999999999999999999959342418532
double: 0.02000000000000000041633363423443

现在您可以看到问题所在。当您添加 printf 时,average 被强制为 double,将其推高至 0.02 以上。但是,如果没有 printfaverage 在其原生扩展精度格式中将略小于 0.02。

当您执行int a=temp*100; 时,会出现错误。如果没有转换,这将生成a = 1。通过转换,这使得a = 2

要解决这个问题,只需使用int a=round(temp*100); - 你所有的奇怪错误都会消失。


值得注意的是,此错误对代码的更改非常敏感。任何导致寄存器被保存的东西(例如几乎任何地方的printf)实际上都会导致错误消失。因此,这是一个非常好的 heisenbug 示例:当您尝试调查它时会消失的错误。

【讨论】:

  • 嗯,我还担心 0.0199999... 只是略低于 0.02。但是,IEEE/ISO 规则不应该为浮点运算指定结果吗?因此,只有在使用 -ffast-math 标志(或类似标志)时才会出现差异吗?
  • (int)(0.019999 * 100) 始终为 1。而且,我们不知道 IDEOne 是否使用-ffast-math(我似乎找不到他们的编译器标志)。我也知道浮点运算只需要精确到 ulp 的各个分数,但这仍然是足够的错误,结果可能高于/低于 0.02 而不会影响该保证。
  • 是的,static volatile double dummy = average 做同样的事情。
  • @mastov:标准不要求确定性行为,它要求有界错误行为。这是一个关键的区别。允许计算结果与真实的数学结果有一定的偏差,通常以最后位置单位 (ulp) 的倍数指定。不同的实现可以使用不同的算法,只要它们在这个误差范围内,并且浮点答案实际上可能在不同的系统中有所不同。
  • @mastov:C99 不强制要求 IEEE754。它说:“定义__STDC_IEC_559__ 的实现应符合本附件中的规范。[基本上是 IEEE754]”。
【解决方案2】:

@nneonneo 很好地回答了大部分问题:略有不同的编译导致浮点代码略有不同,导致几乎相同的double 答案,除了一个答案略低于 2.0 和另一个在 2.0 左右。

想补充一点关于不使用转换为int 进行浮点舍入的重要性。

double temp; ... int a=temp*100; 之类的代码强调了这种差异,导致 a 的值为 1 或 2,因为转换为 int 是有效的“向零截断” - 删除分数。

而不是使用如下代码四舍五入到接近 0.01:

double temp;
...
int a = temp*100;  // Problem is here
temp = a/100.0;

根本不要使用int。使用

double temp;
...
temp = round(temp*100.0)/100.0;

这不仅提供了更一致的答案(因为temp 不太可能在half-cent 附近有值),它还允许temp 值超出int 范围。 temp = 1e13; int a = temp/100; 肯定会导致未定义的行为。

不要使用转换为int 来舍入浮点数:使用round()

roundf()roundl()floor()ceil() 等也可能有用。 @jeff

【讨论】:

    【解决方案3】:

    您将双精度数除以整数,即整数除法。在这种情况下,它将给出一个值 0。

    如果您将 student 转换为 double,它应该会给您正确的输出。

    average=average/(double)students;
    

    根据您的算法,可能还需要其他位置。

    【讨论】:

    • 什么? double/int 是双除法。我不知道你在说什么。
    • 但是为什么删除 printf 或添加它会改变我的答案
    • 是的,我认为 double/int 是双除法
    • @AdityaSharma 确实如此,而且这个答案既不正确,也与您的问题无关。如果我更改了您代码中的任何内容,我将删除所有 100x 并严格使用 double,使用 floorceil 进行您认为需要的任何舍入。
    猜你喜欢
    • 2019-05-06
    • 2017-11-29
    • 2015-05-03
    • 2017-05-20
    • 2016-12-12
    • 1970-01-01
    • 1970-01-01
    • 2016-04-04
    • 1970-01-01
    相关资源
    最近更新 更多