Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different Answers by removing a printf statement

Tags:

c

This is the link to the question on UVa online judge.
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=29&page=show_problem&problem=1078

My C code is

#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;
}

One of the input and outputs are
Input
3
0.01
0.03
0.03
0

Output
$0.01

My output is
$0.00.

However if I uncomment the line printf("%lf",average);

The Output is as follows
0.02 //This is the average
$0.01

I am running the code on ideone.com

Please explain why is this happening.

like image 842
Aditya Sharma Avatar asked Jun 13 '15 16:06

Aditya Sharma


2 Answers

I believe I've found the culprit and a reasonable explanation.

On x86 processors, the FPU operates internally with extended precision, which is an 80-bit format. All of the floating point instructions operate with this precision. If a double is actually required, the compiler will generate code to convert the extended precision value down to a double precision value. Crucially, the commented-out printf forces such a conversion because the FPU registers must be saved and restored across that function call, and they will be saved as doubles (note that avg and mon are both inlined so no save/restore happens there).

In fact, instead of printf we can use the line static double dummy = average; to force the double conversion to occur, which also causes the bug to disappear: http://ideone.com/a1wadn

Your value of average is close to but not exactly 0.02 because of floating-point inaccuracies. When I do all the calculations explicitly with long double, and I print out the value of average, it is the following:

long double: 0.01999999999999999999959342418532
double: 0.02000000000000000041633363423443

Now you can see the problem. When you add the printf, average is forced to a double which pushes it above 0.02. But, without the printf, average will be a little less than 0.02 in its native extended precision format.

When you do int a=temp*100; the bug appears. Without the conversion, this makes a = 1. With the conversion, this makes a = 2.

To fix this, simply use int a=round(temp*100); - all your weird errors should vanish.


Of note, this bug is extremely sensitive to changes in the code. Anything that causes the registers to be saved (such as a printf pretty much anywhere) will actually cause the bug to vanish. Hence, this is an extremely good example of a heisenbug: a bug that vanishes when you try to investigate it.

like image 58
nneonneo Avatar answered Nov 06 '22 08:11

nneonneo


@nneonneo well answered most of the issue: Slightly variant compilations result in slightly different floating-point code that result in nearly the same double answer, except one answer is just below 2.0 and the the other at or just about 2.0.

Like to add about the importance on not using conversion to int for floating point rounding.

Code like double temp; ... int a=temp*100; accentuate this difference resulting in a with a value of 1 or 2 as conversion to int is effective "truncate toward zero" - drop the fraction.

Rather than round to near 0.01 with code like:

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

Do not use int at all. Use

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

Not only does this provide more consistent answers (as temp is unlikely to have values near a half-cent), it also allows temp values outside the int range. temp = 1e13; int a = temp/100; certainly results in undefined behavior.

Do not use conversion to int to round floating-point numbers: use round()

roundf(), roundl(), floor(), ceil(), etc. may also be useful. @jeff

like image 31
chux - Reinstate Monica Avatar answered Nov 06 '22 07:11

chux - Reinstate Monica