Fast Math
Overview
Audio DSP calls transcendental functions constantly: an oscillator wants a sine every sample, a compressor converts to and from decibel (log10 / pow10) on every gain update, a soft clipper reaches for tanh. The standard-library versions are correctly rounded to the last bit, and pay for it in cycles. In a per-sample inner loop that accuracy is usually wasted: the result is about to be quantized, summed with noise, and turned into sound. Q therefore ships a family of fast approximations that trade a little accuracy for a large speed-up.
Every function comes in two tiers, distinguished by name:
-
fast_*— the accurate tier. Error is small enough to be inaudible in almost any audio use (a few parts in 10^-5 for the bounded functions). This is the sensible default. -
faster_*— the cheap tier. It drops a polynomial term or two, so the error grows to the low percent range, in exchange for the fewest possible operations. Reach for it only where the result feeds something forgiving (a modulation source, a rough envelope) and the cycles genuinely matter.
The approximations are IEEE-754 bit tricks and short polynomials, from Paul Mineiro’s fastapprox. They take and return float; there are no double overloads.
The error is not a fixed bias: it ripples as the input sweeps, because each approximation is a smooth curve fitted to the true function, running slightly high then slightly low between the points where it crosses the exact value. fast_* fits closely, faster_* loosely, but neither introduces discontinuities, so both are safe to modulate. The worst-case sizes are tabulated in Accuracy and Speed.
Most of these functions are only valid over a restricted domain, listed per function below. The trigonometric approximations in particular assume the input is already range-reduced; feeding them an angle outside the stated range gives a wrong answer, not a clamped one.
fast_sqrt is a deliberate exception. It is a plain alias for std::sqrt, which lowers to a single hardware instruction on every target Q supports and is both exact and the fastest option available. It is kept in the family only so existing call sites need not change.
|
The fast_* trigonometric functions do no range reduction. fast_sin and fast_cos require -pi <= x <= pi; fast_tan requires -pi/2 <= x <= pi/2. Outside those ranges the polynomial diverges. Reduce the angle yourself (for example with a phase accumulator, which wraps by construction) before calling.
|
Declaration
namespace cycfi::q
{
// Exponential and logarithm
inline float fast_exp(float x); inline float faster_exp(float x);
inline float fast_log(float x); inline float faster_log(float x);
inline float fast_log2(float x); inline float faster_log2(float x);
inline float fast_log10(float x); inline float faster_log10(float x);
inline float fast_pow2(float x); inline float faster_pow2(float x);
inline float fast_pow10(float x); inline float faster_pow10(float x);
// Trigonometric (sin/cos: [-pi, pi]; tan: [-pi/2, pi/2])
inline float fast_sin(float x); inline float faster_sin(float x);
inline float fast_cos(float x); inline float faster_cos(float x);
inline float fast_tan(float x); inline float faster_tan(float x);
// Hyperbolic tangent (valid over all reals; saturates to +/-1)
inline float fast_tanh(float x); inline float faster_tanh(float x);
constexpr float fast_rational_tanh(float x); // Pade, -3 <= x <= 3
// Reciprocal and division
inline float fast_inverse(float val);
inline float fast_div(float a, float b);
// Square root (exact: alias for std::sqrt)
inline float fast_sqrt(float x);
// Taylor-series exp, order 3..9 (small x)
constexpr float fast_exp3(float x); // ... through ...
constexpr float fast_exp9(float x);
// Fast integer RNG
inline int fast_rand(); // result in [0, 0x7FFF]
}
Expressions
Exponential and Logarithm
The logarithms require x > 0. The exponentials are valid over all reals; underflow is handled (a large negative input returns 0, not a denormal or NaN).
| Expression | Semantics | Return Type |
|---|---|---|
|
e raised to |
|
|
Natural logarithm of |
|
|
Base-2 logarithm of |
|
|
Base-10 logarithm of |
|
|
2 raised to |
|
|
10 raised to |
|
Each row has a faster_ counterpart with the same signature and semantics but larger error (see Accuracy and Speed).
fast_pow10 computes pow2(x * log2(10)) using the exact log2(10), rather than routing through a generic pow that would scale by an approximate constant. Folding in the exact constant costs the same single multiply yet is markedly more accurate; against std::pow, fast_pow10 RMSE drops about 5x and faster_pow10 about 2x.
|
Trigonometric
Range-reduced only. fast_sin / fast_cos assume -pi <= x <= pi; fast_tan assumes -pi/2 <= x <= pi/2.
| Expression | Semantics | Return Type |
|---|---|---|
|
Sine of |
|
|
Cosine of |
|
|
Tangent of |
|
Each has a faster_ counterpart.
Hyperbolic Tangent
A staple nonlinearity for saturation and soft clipping.
| Expression | Semantics | Return Type |
|---|---|---|
|
Hyperbolic tangent, valid over all reals; saturates to
|
|
|
Cheaper, less accurate |
|
|
Pade rational approximation, |
|
Reciprocal, Division, and Square Root
| Expression | Semantics | Return Type |
|---|---|---|
|
Approximate reciprocal |
|
|
|
|
|
Square root, |
|
fast_inverse (and therefore fast_div) is a low-accuracy estimate, good to only a few percent. Use it for control-rate math where the imprecision washes out, not where you need a faithful quotient.
|
Polynomial and Utility
| Expression | Semantics | Return Type |
|---|---|---|
|
|
|
|
Fast integer pseudo-random number in |
|
The bit-trick functions (fast_exp, fast_log*, fast_pow*, the trig and fast_tanh families, fast_inverse) are inline, not constexpr; they rely on runtime type punning. Only fast_rational_tanh and fast_exp3..fast_exp9 are constexpr.
|
Accuracy and Speed
Accuracy is a clean number: the worst-case error over each function’s working domain. It is small enough to be inaudible for the fast_* tier and merely coarse for faster_*. The figures below were measured with clang -O3 on an Apple-silicon Mac (scalar path); treat them as representative rather than guaranteed, since they shift with compiler, flags, and target.
| Function | fast_* |
faster_* |
|---|---|---|
|
~1.5e-4 (output bits) |
~0.05 (output bits) |
|
~0.006% (relative) |
~4% (relative) |
|
~4e-5 (absolute) |
~9e-4 (absolute) |
|
~3e-5 (absolute) |
~2e-2 (absolute) |
Speed is not a clean number, and deserves a caution rather than a single headline. How much an approximation saves depends on whether the calls are independent, so the CPU pipelines many at once (throughput), or chained, each feeding the next (latency), and for these functions the two differ by up to an order of magnitude. The large wins are in throughput; inside a tight dependency chain the fixed pipeline latency dominates and the saving shrinks. So the honest guidance is to measure on your own target, which the build-only micro-benchmarks under test/benchmark/ exist to do.
decibel_bench.cpp is the most careful of them, reporting both metrics for the log10 / pow10 pair used by decibel:
| Function | Metric | std | fast_* |
faster_* |
|---|---|---|---|---|
|
throughput |
5.8 |
0.31 |
0.16 |
latency |
20 |
14 |
12 |
|
|
throughput |
2.5 |
0.46 |
0.21 |
latency |
23 |
19 |
11 |
That contrast is the caveat made concrete: in throughput fast_log10 is roughly twenty times cheaper than std::log10, but in a dependency chain only about 1.4 times. log2_bench.cpp and sin_bench.cpp measure log2 and sin with a simpler dependency-bound loop (nearer the latency column): there std::sin runs about 12 ns against roughly 2 ns for both fast_sin and faster_sin, and std::log2 about 8 ns against 2.5 ns.
On a modern desktop FPU the faster_* tier is often no quicker than fast_*: both already collapse to a handful of instructions, so dropping a polynomial term is lost in pipeline latency. faster_* pulls ahead on cheaper hardware, where every multiply counts, and under vectorization. The dependable win everywhere is fast_* over the libc call. Rebuild any of these numbers on your own target by running the benchmarks.
|
Example
A cheap saturating soft clipper, one tanh per sample:
float drive(float x, float amount)
{
return q::fast_tanh(x * amount);
}
Converting a linear amplitude to decibel and back in a gain stage, where log10 / pow10 would otherwise dominate the cost:
float g_db = 20.0f * q::fast_log10(amplitude); // linear -> dB
// ... adjust g_db ...
float g = q::fast_pow10(g_db * 0.05f); // dB -> linear
A range-reduced sine oscillator. fast_sin does no wrapping of its own, so the phase is accumulated in [-pi, pi) and folded back each sample to stay inside the valid domain:
float phase = 0.0f; // in [-pi, pi)
float incr = 2.0f * float(q::pi) * 440.0f / sps; // radians per sample
// ... per sample:
float s = q::fast_sin(phase);
phase += incr;
if (phase >= float(q::pi))
phase -= 2.0f * float(q::pi); // fold back into range
For production oscillators prefer the phase / phase_iterator accumulator, whose fixed-point counter wraps by construction, or the table-based Sine Wave Oscillator. The snippet above is here to make the domain restriction concrete.
|