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 *...