Compare Math.sin and lookup table performance
by Ben Gillbanks
JavaScript
// Benchmark: Math.sin/Math.cos vs lookup table (with/without interpolation)
// Run in Node or your browser console.
// Config
const ITERATIONS = 20_000_000; // tweak this if your machine groans
const LUT_SIZE = 1 << 16; // power of two helps indexing
const USE_FLOAT32 = true; // toggle to test Float32 vs Float64 storage
const TEST_SIN = true;
const TEST_COS = true;
const TEST_LERP = true; // tests linear interpolation variant
// Build LUTs
const TwoPI = Math.PI * 2;
const invTwoPI = 1 / TwoPI;
const ArrayType = USE_FLOAT32 ? Float32Array : Float64Array;
const sinLUT = new ArrayType(LUT_SIZE);
const cosLUT = new ArrayType(LUT_SIZE);
for (let i = 0; i < LUT_SIZE; i++) {
const t = (i + 0.5) / LUT_SIZE; // centre of bin to reduce bias
const angle = t * TwoPI;
sinLUT[i] = Math.sin(angle);
cosLUT[i] = Math.cos(angle);
}
// Helpers
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
const mask = LUT_SIZE - 1;
// Map angle in [0, 2π) to LUT index
function indexFor(angle) {
// angle assumed in [0, 2π). If not, normalise before calling for a fair test.
// Fast float -> int
return (angle * invTwoPI * LUT_SIZE) | 0;
}
// Nearest-neighbour lookup
function sinLUT_nn(angle) {
const i = indexFor(angle) & mask;
return sinLUT[i];
}
function cosLUT_nn(angle) {
const i = indexFor(angle) & mask;
return cosLUT[i];
}
// Linear interpolation lookup
function sinLUT_lerp(angle) {
const x = angle * invTwoPI * LUT_SIZE;
const i0 = x | 0;
const i1 = (i0 + 1) & mask;
const f = x - i0;
return sinLUT[i0 & mask] * (1 - f) + sinLUT[i1] * f;
}
function cosLUT_lerp(angle) {
const x = angle * invTwoPI * LUT_SIZE;
const i0 = x | 0;
const i1 = (i0 + 1) & mask;
const f = x - i0;
return cosLUT[i0 & mask] * (1 - f) + cosLUT[i1] * f;
}
// Simple angle generator: walks [0, 2π) with an irrational step to avoid repeating patterns
function* angleGen(n) {
const step = TwoPI *...