views:

584

answers:

7

I'm doing some trigonometry calculations in C/C++ and am running into problems with rounding errors. For example, on my Linux system:

#include <stdio.h>
#include <math.h>

int main(int argc, char *argv[]) {
    printf("%e\n", sin(M_PI));
    return 0;
}

This program gives the following output:

1.224647e-16

when the correct answer is of course 0.

How much rounding error can I expect when using trig functions? How can I best handle that error? I'm familiar with the Units in Last Place technique for comparing floating point numbers, from Bruce Dawson's Comparing Floating Point Numbers, but that doesn't seem to work here, since 0 and 1.22e-16 are quite a few ULPs apart.

+8  A: 

The answer is only 0 for sin(pi) - did you include all the digits of Pi ?

-Has anyone else noticed a distinct lack of, irony/sense of humour around here?

Martin Beckett
I'm using M_PI verbatim out of math.h. If the compiler isn't including all of its digits, then something is seriously wrong. :-)
Josh Kelley
Pi has an infinte number of digits so I very much doubt it's including them all. What you're seeing is really close to 0 (0.00000000000000001), especially for a double.
Blindy
Perhaps your math.h doesn't include all the digits of pi? Do you have an infinite number of hard drives?
Martin Beckett
I should elaborate - M_PI is, as far as I know, as many digits of pi as the double type can hold. If that's not enough digits to give a result of 0, then that at least explains where the imprecision is coming from, but I still need a way to handle that imprecision.
Josh Kelley
The representation of PI is fine. It's just the limit of double precision.
Andy Ross
@mgb: It's all on one of my infinite-length Turing machine tapes. (Man, try getting Purchasing to sign off on one of those!)
David Thornley
Actually if PI is only defined to the accuracy limit of float then any operation on it that returns a float can only have poorer accuracy - it was in that boring lecture on entropy and theory of computability.
Martin Beckett
@Josh Kelley - "I still need a way to handle that imprecision." Use the GMP BigNum library, or another arbitrary-precision math library.
Chris Lutz
Because PI is an irrational number with an infinite, it is impossible to include all digits. The best you can do is determine how accurate you would like your result and round the number to the corresponding significant digit of accuracy. Any calculation that does not round the results to a significant digit will display results that are not expected. This is the case here where the results are showing 1.22E-16, which is actually 0.000000000000000122. As you can see this number is practically 0 and if you round to a set significant digit lower than 16 digits you will receive a result of 0.
Matt Pascoe
A: 

I rather think that will be system-dependent. I don't think the Standard has anything to say on how accurate the transcendental functions will be. Unfortunately, I don't remember seeing any discussion of function precision, so you'll probably have to figure it out yourself.

David Thornley
The math functions on all major platforms are accurate to within maximal double-precision generally. The only exception I can think of is that the on x86 (which does trancendentals in hardware) you can play games with the FPU precision mode to trade accuracy for performance.
Andy Ross
Actually the internal FPU in Intel processors uses an 80 bit representation in its interal registers, so it's results are much more accruate than a double allows as long as you don't truncate intermediate results by storing them in a 64bit double variable.
Stephen C. Steel
Wrong - the standard is actually very definite about the precision of the math functions. See my answer.
DevSolar
I have to apologize. While trying to find the exact section of the C standard that states the precision, I realized I had been confusing two things in my memory - the C standard and the 68881/68882 FPU documentation. :-D The C standard states that the precision of the math functions is "implementation defined".
DevSolar
+10  A: 

An IEEE double stores 52 bits of mantissa, with the "implicit leading one" forming a 53 bit number. An error in the bottom bit of a result therefore makes up about 1/2^53 of the scale of the numbers. Your output is of the same order as 1.0, so that comes out to just about exactly one part in 10^16 (because 53*log(2)/log(10) == 15.9).

So yes. This is about the limit of the precision you can expect. I'm not sure what the ULP technique you're using is, but I suspect you're applying it wrong.

Andy Ross
The ULP technique as described in the link in the original post will have this problem for answers close to zero. I suspect sin(M_PI/2.0) will come out to some number c. 10^-16 away from 1, but in that case, the ULP technique should work.
Chris Johnson
So because the output of sin, cos and tan is in the range [0, 1], I can expect results to be off by no more than ~10e-16 (which, as you said, is one ULP from 1). Is that correct? (`__DBL_EPSILON__` in C and `std::numeric_limits<double>::epsilon()` in C++ provide this ~10e-16 number.)
Josh Kelley
For angles of order 1, you can expect accuracy of a few*10e-16 from sin and cos (which output in the range [-1,1]). With tan=sin/cos, the output range is unbounded, and where the accuracy is best is likely to depend on how the tan function has been written. The absolute and fractional errors will both be at least 10e-16 though.
Chris Johnson
Keep in mind, you're not calculating sin(pi), but sin(M_PI)! The constant M_PI is not exactly Pi, but rather the closest approximation representable as a double: M_PI = (Pi + delta), where delta is ~10e-16. Combine this with expansion of sin(Pi+x) = -x for |x| << 1, and it's not at all surprising that sin(M_PI) is yielding a result this small.
Stephen C. Steel
+5  A: 

@Josh Kelley - ok serious answer.
In general you should never compare the results of any operation involving floats or doubles with each other.

The only exceptions is assignment.
float a=10.0;
float b=10.0;
then a==b

Otherwise you always have to write some function like bool IsClose(float a,float b, float error) to allow you to check if two numbers are within 'error' of each other.
Remember to also check signs/use fabs - you could have -1.224647e-16

Martin Beckett
I'm trying to figure out what an appropriate error would be for an IsClose-style comparison. I like the Units in Last Place technique that I linked to because it's supposed to obviate the need to pick an appropriate error, but it apparently doesn't work in this case.
Josh Kelley
It would depend on your real world application. If you are just drawing circles on screen and need to know when you have reached the top then 0.1deg would probably be enough. If you are doing RTK GPS and need a lat/long to mm then you need 10E-10.You very rarely need/ or want to work in the area where a LSP matters.
Martin Beckett
A: 

I get the exact same result on my system - I'd say it is close enough

I would solve the problem by changing the format string to "%f\n" :)

However, this gives you a "better" result, or at least on my system it does give -3.661369e-245

#include <stdio.h>
#include <math.h>

int main(int argc, char *argv[]) {
    printf("%e\n", (long double)sin(M_PI));
    return 0;
}
Kimvais
C99 has `long double sinl(long double x);`.
pmg
A: 

Unless your program requires significant digits out to the 16th decimal place or more, you probably can do the rounding manually. From my experience programming games we always rounded our decimals to a tolerable significant digit. For example:

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#define HALF 0.5
#define GREATER_EQUAL_HALF(X) (X) >= HALF

double const M_PI = 2 * acos(0.0);

double round(double val, unsigned  places = 1) 
{
    val = val * pow(10.0f, (float)places);
    long longval = (long)val;
    if ( GREATER_EQUAL_HALF(val - longval) ) {
       return ceil(val) / pow(10.0f, (float)places);
    } else {
      return floor(val) / pow(10.0f, (float)places);
    }
}

int main() 
{
    printf("\nValue %lf", round(sin(M_PI), 10));
    return 0;
}
Matt Pascoe
Note that `double round(double);` is a function in the C99 standard library, and that production code should use another name for it. Also, this is a C question, not a C++ one, so no `<cmath>` and friends.
Chris Lutz
In addition to the previous, it appears that ISO no longer includes M_PI as a constant in the C99 standard library. Using M_PI will not work in all compiliers, like Visual Studio.
Matt Pascoe
+1  A: 

There are two sources of error. The sin() function and the approximated value of M_PI. Even if the sin() function were 'perfect', it would not return zero unless the value of M_PI were also perfect - which it is not.

Clifford