Skip to main content
4 of 4
Explicitly casting to double, just to make sure!
user avatar
user avatar

Octave (or Matlab) & C

A bit of a complicated build process, but kind of a novel approach and the results were encouraging.

The approach is to generate approximating quadratic polynomials for each degree. So degree = [0, 1), degree = [1, 2), ..., degree = [359, 360) will each have a different polynomial.

Octave - building part

Octave is publicly available - Google download octave.

This determines the best fit quadratic polynomial for every degree.

Save as build-fast-trig.m:

format long;
for d = 0:359
    x = (d-1):0.1:(d+1);
    y = sin(x / 360 * 2 * pi);
    polyfit(x, y, 2)
endfor

C - building part

This converts doubles in text format to native binary format on your system.

Save as build-fast-trig.c:

#include <stdio.h>

int main()
{
    double d[3];

    while (scanf("%lf %lf %lf", d, d + 1, d + 2) == 3)
        fwrite(d, sizeof(double), 3, stdout);

    return 0;
}

Compile:

gcc -o build-fast-trig build-fast-trig.c

Generating the coefficients file

Run:

octave build-fast-trig.m | grep '^ ' | ./build-fast-trig > qcoeffs.dat

Now we have qcoeffs.dat as the data file to use for the actual program.

C - fast-trig part

Save as fast-trig.c:

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

#define INPUT    "qcoeffs.dat"

#define DEGREES    360

typedef struct {double a, b, c;} QCOEFFS;

double normalize(double d)
{
    if (d < 0.0)
        d += ceil(d / -(double)DEGREES) * (double)DEGREES;

    if (d >= (double)DEGREES)
        d -= floor(d / (double)DEGREES) * (double)DEGREES;

    return d;
}

int main()
{
    FILE *f;
    time_t tm;
    double d;
    QCOEFFS qc[DEGREES];

    if (!(f = fopen(INPUT, "rb")) || fread(qc, sizeof(QCOEFFS), DEGREES, f) < DEGREES)
    {
        fprintf(stderr, "Problem with %s - aborting.", INPUT);
        return EXIT_FAILURE;
    }
    fclose(f);

    tm = -clock();

    while (scanf("%lf", &d) > 0)
    {
        int i;
        double e, f;

        /* sin */
        d = normalize(d);
        i = (int)d;
        e = (qc[i].a * d + qc[i].b) * d + qc[i].c;

        /* cos */
        d = normalize((double)DEGREES / 4.0 - d);
        i = (int)d;
        f = (qc[i].a * d + qc[i].b) * d + qc[i].c;

        /* tan = sin / cos */

        /* output - format closest to specs, AFAICT */
        if (d != 0.0 && d != 180.0)
            printf("%.6e %.6e %.6e\n", e, f, e / f);
        else
            printf("%.6e %.6e n\n", e, f);
    }

    tm += clock();

    fprintf(stderr, "time: %.3fs\n", (double)tm/(double)CLOCKS_PER_SEC);    

    return EXIT_SUCCESS;
}

Compile:

gcc -o fast-trig fast-trig.c -lm

Run:

./fast-trig < trig.in > trig.out

It will read from trig.in, save to trig.out and print to console the elapsed time with millisecond precision.

Depending on the testing methods used it may fail on certain input, e.g.:

$ ./fast-trig 
0
-6.194924e-19 1.000000e+00 -6.194924e-19

The correct output should be 0.000000e+00 1.000000e+00 0.000000e+00. If the results are validated using strings, the input will fail, if they are validated using absolute error, e.g. fabs(actual - result) < 1e-06, the input will pass.

The maximum absolute error for sin and cos was ≤ 3e-07. For tan, because the result isn't limited to ± 1 and you can divide a relatively large number by a relatively small number, the absolute error could be larger. From -1 ≤ tan(x) ≤ +1, the maximum absolute error was ≤ 4e-07. For tan(x) > 1 and tan(x) < -1, the maximum relative error, e.g. fabs((actual - result) / actual) was usually < 1e-06 until you get in the area of (90 ± 5) or (270 ± 5) degrees, then the error gets worse.

In testing, the average time per single input was (1.053 ± 0.007) µs, which on my machine was about 0.070 µs faster than native sin and cos, tan being defined the same way.

user15259