Original Post
This is a extremely fast sin approximation I have been working on for a while and I thought that I'd share it. Before starting off with the code and how I derived this approximation, let's start off with some data:
As you can see, this approximation is around 3.9 times as fast as sinf and 8.3 times as fast as the default sin. This was tested with all optimizations turned on. The worst error is 0.000296, meaning that this approximation is very usable. On top of that, on the points 0, pi/6, pi/2 (and multiples of those) fast_sin gives the exact correct results.
Now, this isn't all magic and ponies, there of course are downsides. The biggest speed-up I've gotten is using a fast truncate (more about this later). This method relies on IEEE754 and that the FPU is in double-precision mode. If either of those two aren't available the trick can not be used and we have to do a slow truncate (cast-to-int). This slows the routine down by about a factor 2. It's still a lot faster, but not as fast as it could be.
What implications does this have? Well, for most of us: none. On gamedev here we mostly target Windows and sometimes Mac & Linux. All of those are little endian. And if you don't mess around with your FPU it is most likely in double-precision mode.
Allright, now let's see the code:
Now that doesn't seem to be very clear, with magic constants all over the place. I will explain those later. Note the two macros at the top, you should define BIG_ENDIAN if you're targeting a big-endian system, and if you can not assume IEEE754 or double-precision mode FPU you must define NO_FAST_TRUNCATE (this slows the method down).
Allright, the explanation! First I will explain how I calculate the sine, with some highschool math. I have the idea from this devmaster thread. If you draw the sine function, you can see that in the range [-PI/2, PI/2] looks like a regular fifth degree polynomial. To approximate sin(x) I chose the formula [font=courier new,courier,monospace]f(x) = ax^5 + bx^3 + cx[/font]. In order to get a good approximation I made an equation system with known points of sin(x). I set f(pi/2) to 1, f(pi/6) to 1/2, and the derivative of f(pi/2) to 0 (if a derivative is zero then the formula is "flat" on that point, which is exactly what we want). This results in the following equation system and solution:
However, we still have a major problem. This is only usable in the range [-PI/2, PI/2] and not outside of it! This is possible to solve with a modulus, but that is way to expensive. Instead I divide x by PI (or multiply by 1/PI) and then round it to an integer. Then I substract that integer times PI from the original x to simulate a modulo.
We have two more problems. If x is in the range [PI/2, PI], sin(x) is negative. So far we haven't accounted for that. The easiest solution is to re-use the integer calculated above. If that integer is odd, we are in the negative part of the sine function, and we must flip our result.
Our last problem is rounding a double to an integer. In my original code I found that 50% of the time is spent in the [font=courier new,courier,monospace]round[/font] function. After searching the internet for a while I found this page by Sree Kotay giving a solution. And it works, beautifully, solving the final problem.
The great part about this also is is that it doesn't rely on the standard library at all. This means that this is very usable for demos and such (that often don't link to the standard library for code size).
And finally a "cool" version that has every comment and useful stuff removed. This version only works in a little endian, IEEE754 with double-precision FPU environment (which is pretty much the default Windows environment), but in the way it's written is very easily converted to assembler:
fast_sin time: 148.4ms
sinf time: 572.7ms
sin time: 1231.2ms
Worst error: 0.000296
Average error: 0.000124
Average relative error: 0.02%As you can see, this approximation is around 3.9 times as fast as sinf and 8.3 times as fast as the default sin. This was tested with all optimizations turned on. The worst error is 0.000296, meaning that this approximation is very usable. On top of that, on the points 0, pi/6, pi/2 (and multiples of those) fast_sin gives the exact correct results.
Now, this isn't all magic and ponies, there of course are downsides. The biggest speed-up I've gotten is using a fast truncate (more about this later). This method relies on IEEE754 and that the FPU is in double-precision mode. If either of those two aren't available the trick can not be used and we have to do a slow truncate (cast-to-int). This slows the routine down by about a factor 2. It's still a lot faster, but not as fast as it could be.
What implications does this have? Well, for most of us: none. On gamedev here we mostly target Windows and sometimes Mac & Linux. All of those are little endian. And if you don't mess around with your FPU it is most likely in double-precision mode.
Allright, now let's see the code:
/* uncomment the next line if you're on a big-endian system */
/* #define BIG_ENDIAN */
/* uncomment the next line if you can not assume double-precision FPU or IEEE754 */
/* #define NO_FAST_TRUNCATE */
/* we need to do some custom hacking for MSVC */
#ifdef _MSC_VER
typedef __int32 int32_t;
#else
#include <stdint.h>
#endif
inline int32_t fast_round(double x) {
#ifndef NO_FAST_TRUNCATE
const double MAGIC_ROUND = 6755399441055744.0; /* http://stereopsis.com/sree/fpu2006.html */
union {
double d;
struct {
#ifdef BIG_ENDIAN
int32_t hw;
int32_t lw;
#else
int32_t lw;
int32_t hw;
#endif
};
} fast_trunc;
fast_trunc.d = x;
fast_trunc.d += MAGIC_ROUND;
return fast_trunc.lw;
#else
if (x < 0) {
return (int32_t) (x - 0.5);
} else {
return (int32_t) (x + 0.5);
}
#endif
}
inline double fast_sin(double x) {
const double PI = 3.14159265358979323846264338327950288;
const double INVPI = 0.31830988618379067153776752674502872;
const double A = 0.00735246819687011731341356165096815;
const double B = -0.16528911397014738207016302002888890;
const double C = 0.99969198629596757779830113868360584;
int32_t k;
double x2;
/* find offset of x from the range -pi/2 to pi/2 */
k = fast_round(INVPI * x);
/* bring x into range */
x -= k * PI;
/* calculate sine */
x2 = x * x;
x = x*(C + x2*(B + A*x2));
/* if x is in an odd pi count we must flip */
if (k % 2) x = -x;
return x;
}Now that doesn't seem to be very clear, with magic constants all over the place. I will explain those later. Note the two macros at the top, you should define BIG_ENDIAN if you're targeting a big-endian system, and if you can not assume IEEE754 or double-precision mode FPU you must define NO_FAST_TRUNCATE (this slows the method down).
Allright, the explanation! First I will explain how I calculate the sine, with some highschool math. I have the idea from this devmaster thread. If you draw the sine function, you can see that in the range [-PI/2, PI/2] looks like a regular fifth degree polynomial. To approximate sin(x) I chose the formula [font=courier new,courier,monospace]f(x) = ax^5 + bx^3 + cx[/font]. In order to get a good approximation I made an equation system with known points of sin(x). I set f(pi/2) to 1, f(pi/6) to 1/2, and the derivative of f(pi/2) to 0 (if a derivative is zero then the formula is "flat" on that point, which is exactly what we want). This results in the following equation system and solution:
Equations:
f(x) = ax^5 + bx^3 + c
f(pi/2) = 1
f'(pi/2) = 0
f(pi/6) = 1/2
Solution:
a = 9 / (4 * pi^5)
b = -41 / (8 * pi^3)
c = 201 / (64 * pi)However, we still have a major problem. This is only usable in the range [-PI/2, PI/2] and not outside of it! This is possible to solve with a modulus, but that is way to expensive. Instead I divide x by PI (or multiply by 1/PI) and then round it to an integer. Then I substract that integer times PI from the original x to simulate a modulo.
We have two more problems. If x is in the range [PI/2, PI], sin(x) is negative. So far we haven't accounted for that. The easiest solution is to re-use the integer calculated above. If that integer is odd, we are in the negative part of the sine function, and we must flip our result.
Our last problem is rounding a double to an integer. In my original code I found that 50% of the time is spent in the [font=courier new,courier,monospace]round[/font] function. After searching the internet for a while I found this page by Sree Kotay giving a solution. And it works, beautifully, solving the final problem.
The great part about this also is is that it doesn't rely on the standard library at all. This means that this is very usable for demos and such (that often don't link to the standard library for code size).
And finally a "cool" version that has every comment and useful stuff removed. This version only works in a little endian, IEEE754 with double-precision FPU environment (which is pretty much the default Windows environment), but in the way it's written is very easily converted to assembler:
double fast_sin(double x) {
int k;
double y;
double z;
z = x;
z *= 0.3183098861837907;
z += 6755399441055744.0;
k = *((int *) &z);
z = k;
z *= 3.1415926535897932;
x -= z;
y = x;
y *= x;
z = 0.0073524681968701;
z *= y;
z -= 0.1652891139701474;
z *= y;
z += 0.9996919862959676;
x *= z;
k &= 1;
k += k;
z = k;
z *= x;
x -= z;
return x;
}