Ivan Perez
Ivan Perez

Reputation: 610

Float inputs for which sinf and sin return different results?

I'm trying to understand something about sin and sinf from math.h.

I understand that their types differ: the former takes and returns doubles, and the latter takes and returns floats.

However, GCC still compiles my code if I call sin with float arguments:

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

#define PI 3.14159265

int main ()
{
  float x, result;
  x = 135 / 180 * PI;
  result = sin (x);
  printf ("The sin of (x=%f) is %f\n", x, result);
  return 0;
}

By default, all compiles just fine (even with -Wall, -std=c99 and -Wpedantic; I need to work with C99). GCC won't complain about me passing floats to sin. If I enable -Wconversion then GCC tells me:

warning: conversion to ‘float’ from ‘double’ may alter its value [-Wfloat-conversion]
   result = sin (x);
            ^~~

So my question is: is there a float input for which using sin, like above, and (implicitly) casting the result back to float, will result in a value that is different from that obtained using sinf?

Upvotes: 1

Views: 1030

Answers (5)

supercat
supercat

Reputation: 81179

There are some systems where computations on float are an order of magnitude faster than computations on double. The primary purpose of sinf is to allow trigonometric calculations to be performed efficiently on such systems in cases where the lower precision of float would be adequate to satisfy application needs. Converting a value to float, calling sin, and converting the result to float would always yield a value that either matched that of sinf or was more accurate(*), and on some implementations that would in fact be the most efficient way of implementing sinf. On some other systems, however, such an approach would be more than an order of magnitude slower than using a purpose-designed function to evaluate the sine of a float.

(*) Note that for arguments outside the range +/- π/2, the most mathematically accurate way of computing sin(x) for an exact specified value of x might not be the most accurate way of computing what the calling code wants to know. If an application computes sinf(angle * (2.0f * 3.14159265f)), when angle is 0.5, having the function (double)3.1415926535897932385-(float)3.14159265f may be more "mathematically accurate" than having it return sin(angle-(2.0f*3.14159265f)), but the latter would more accurately represent the sine of the angle the code was actually interested in.

Upvotes: 0

Luis Colorado
Luis Colorado

Reputation: 12668

However, GCC still compiles my code if I call sin with float arguments:

Yes, this is because they are implicitly converted to double (because sin() requires a float), and back to float (because sin() returns a double) on entering and exiting from the sinf() function. See below why it is better to use sinf() in this case, instead of having only one function.

You have included math.h which has prototypes for both function calls:

double sin(double);
float sinf(float);

And so, the compiler knows that to use sin() it is necessary a conversion from float to double so it compiles a conversion before calling, and also compiles a conversion from double to float in the result from sin().

In case you have not #include <math.h> and you ignored the compiler warning telling you are calling a function sin() with no prototype, the compiler should have also converted first the float to double (because on nonspecified argument types this is how it mus proceed) and pass the double data to the function (which is assumed to return an int in this case, that will provoke a serious Undefined Behaviour)

In case you have used the sinf() function (with the proper prototype), and passed a float, then no conversion should be compiled, the float is passed as such with no type conversion, and the returned value is assigned to a float variable, also with no conversion. So everything goes fine with no conversion, this makes the fastest code.

In case you have used the sinf() function (with no prototype), and passed a float, this float would be converted to a double and passed as such to sinf(), resulting in undefined behaviour. In case somehow sinf() returned properly, an int result (that could have something to do with the calculation or not, as per UB) would be converted into float type (should this be possible) and assigned to the result value.

In the case mentioned above, in case you are operating on floats, it is better to use sinf() as it takes less to execute (it has less iterations to do, as less precision is required in them) and the two conversions (from float to double and back from double to float) have not to be compiled in, in the binary code output by the compiler.

Upvotes: 0

Ian Abbott
Ian Abbott

Reputation: 17403

This will find all the float input values in the range 0.0 to 2 * M_PI where (float)sin(input) != sinf(input):

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

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

int main(void)
{
    for (float in = 0.0; in < 2 * M_PI; in = nextafterf(in, FLT_MAX)) {
        float sin_result = (float)sin(in);
        float sinf_result = sinf(in);
        if (sin_result != sinf_result) {
            printf("sin(%.*g) = %.*g, sinf(%.*g) = %.*g\n",
                   FLT_DECIMAL_DIG, in, FLT_DECIMAL_DIG, sin_result,
                   FLT_DECIMAL_DIG, in, FLT_DECIMAL_DIG, sinf_result);
        }
    }
    return 0;
}

There are 1020963 such inputs on my amd64 Linux system with glibc 2.32.

Upvotes: 4

Clifford
Clifford

Reputation: 93476

float precision is approximately 6 significant figures decimal, while double is good for about 15. (It is approximate because they are binary floating point values not decimal floating point).

As such for example: a double value 1.23456789 will become 1.23456xxx as a float where xxx are unlikely to be 789 in this case.

Clearly not all (in fact very few) double values are exactly representable by float, so will change value when down-converted.

So for:

double a = 1.23456789 ;
float b = a ;
printf( "double: %.10f\n", a ) ;
printf( "float: %.10f\n", b ) ;

The result in my test was:

double: 1.2345678900
float:  1.2345678806

As you can see the float in fact retained 9 significant figures in this case, but it is by no means guaranteed for all possible values.

In your test you have limited the number of instances of mismatch because of the limited and finite range of rand() and also because f itself is float. Consider:

int main()
{
    unsigned mismatch_count = 0 ;
    unsigned iterations = 0 ;
    for( double f = 0; f < 6.28318530718; f += 0.000001) 
    {
        float f1 = sinf(f);
        float f2 = sin(f);
        iterations++ ;
        if(f1 != f2)
        {
            mismatch_count++ ;
        }
    }
    printf("%f%%\n", (double)mismatch_count/iterations* 100.0);}

In my test about 55% of comparisons mismatched. Changing f to float, the mismatches reduced to 1.3%.

So in your test, you see few mismatches because of the constraints of your method of generating f and its type. In the general case the issue is much more obvious.

In some cases you might see no mismatches - an implementation may simply implement sinf() using sin() with explicit casts. The compiler warning is for the general case of implicitly casting a double to a float without reference to any operations performed prior to the conversion.

Upvotes: 0

Steve Summit
Steve Summit

Reputation: 47952

This program finds three examples on my machine:

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

int main()
{
    int i;
    float f, f1, f2;

    for(i = 0; i < 10000; i++) {
        f = (float)rand() / RAND_MAX;
        float f1 = sinf(f);
        float f2 = sin(f);

        if(f1 != f2) printf("jackpot: %.8f %.8f %.8f\n", f, f1, f2);
    }
}

I got:

jackpot: 0.98704159 0.83439910 0.83439904
jackpot: 0.78605396 0.70757037 0.70757031
jackpot: 0.78636044 0.70778692 0.70778686

Upvotes: 3

Related Questions