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.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
Josh Kelley
  • 56,064
  • 19
  • 146
  • 246

9 Answers9

16

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
  • 94,801
  • 28
  • 188
  • 263
  • 2
    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 Oct 06 '09 at 19:30
  • 1
    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 Oct 06 '09 at 19:34
  • 10
    Perhaps your math.h doesn't include all the digits of pi? Do you have an infinite number of hard drives? – Martin Beckett Oct 06 '09 at 19:35
  • 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 Oct 06 '09 at 19:36
  • The representation of PI is fine. It's just the limit of double precision. – Andy Ross Oct 06 '09 at 19:38
  • @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 Oct 06 '09 at 19:46
  • 2
    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 Oct 06 '09 at 19:48
  • 1
    @Josh Kelley - "I still need a way to handle that imprecision." Use the GMP BigNum library, or another arbitrary-precision math library. – Chris Lutz Oct 06 '09 at 22:18
  • 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 Oct 07 '09 at 14:34
14

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
  • 11,699
  • 1
  • 34
  • 31
  • 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 Oct 06 '09 at 19:39
  • 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::epsilon()` in C++ provide this ~10e-16 number.) – Josh Kelley Oct 06 '09 at 20:19
  • 3
    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 Oct 06 '09 at 21:33
  • 7
    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 Oct 06 '09 at 22:21
11

Sine of π is 0.0.
Sine of M_PI is about 1.224647e-16.

M_PI is not π.

program gives ... 1.224647e-16 when the correct answer is of course 0.

Code gave a correct answer to 7 significant places.


The following does not print the sine of π. It prints the sine of a number close to π. See below pic.

π                            // 3.141592653589793 2384626433832795...
printf("%.21\n", M_PI);      // 3.141592653589793 115998
printf("%.21f\n", sin(M_PI));// 0.000000000000000 122465

Note: With the math function sine(x), the slope of the curve is -1.0 at x = π. The difference of π and M_PI is about the sin(M_PI) - as expected.


am running into problems with rounding errors

The rounding problem occurs when using M_PI to represent π. M_PI is the double constant closest to π, yet since π is irrational and all finite double are rational, they must differ - even by a small amount. So not a direct rounding issue with sin(), cos(), tan(). sin(M_PI) simple exposed the issue started with using M_PI - an inexact π.


This problem, with different non-zero results of sin(M_PI), occurs if code used a different FP type like float, long double or double with something other than 53 binary bits of precision. This is not a precision issue so much as a irrational/rational one.

Sine(x) near π

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
5

@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
  • 94,801
  • 28
  • 188
  • 263
  • 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 Oct 06 '09 at 19:45
  • 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 Oct 06 '09 at 19:56
  • I think even integers are represented correctly in float and if `float a=10.0; float b=15.0; float c=25.0` then `a+b==c` holds. – miracle173 Sep 21 '19 at 09:53
1

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
  • 88,407
  • 13
  • 85
  • 165
  • Actually, the requirement for `sin(M_PI)` to return zero would not be for M_PI to be perfect, but rather for mod reduction to be performed as though M_PI was perfect. Except in rare cases where one is trying to simulate a function whose period is a precisely-representable multiple of pi, the arguments to trig functions will often be scaled by M_PI or an equivalent; optimal accuracy may achieved if mod reduction is performed using that same value. – supercat Jun 07 '14 at 18:20
0

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
  • 56,304
  • 9
  • 91
  • 158
  • 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 Oct 06 '09 at 19:39
  • 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 Oct 06 '09 at 22:25
  • Wrong - the standard is actually very definite about the precision of the math functions. See my answer. – DevSolar Oct 07 '09 at 14:28
  • 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 Oct 07 '09 at 14:39
0

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
  • 8,651
  • 17
  • 42
  • 48
  • 3
    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 `` and friends. – Chris Lutz Oct 06 '09 at 22:21
  • 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 Oct 07 '09 at 13:26
-1

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
  • 38,306
  • 16
  • 108
  • 142
-2

Maybe too low accuracy of implementation

M_PI = 3.14159265358979323846 (M_PI is not π)

http://fresh2refresh.com/c/c-function/c-math-h-library-functions/

It is an inaccuracy in implementation, see Stephen C. Steel's comment under Andy Ross` answer above and chux's answer.

E_net4
  • 27,810
  • 13
  • 101
  • 139
ralf htp
  • 9,149
  • 4
  • 22
  • 34