|
1 | | -import rfft from '../index.js' |
| 1 | +import { createRequire } from 'node:module' |
| 2 | +import { fft } from '../index.js' |
2 | 3 |
|
3 | | -const sizes = [256, 1024, 4096, 16384] |
4 | | -const warmup = 1000 |
5 | | -const iterations = 50000 |
| 4 | +const require = createRequire(import.meta.url) |
6 | 5 |
|
7 | | -for (const N of sizes) { |
8 | | - const input = new Float32Array(N) |
9 | | - for (let i = 0; i < N; i++) input[i] = Math.random() * 2 - 1 |
| 6 | +const N = 4096 |
| 7 | +const WARMUP = 500 |
| 8 | +const ITERATIONS = 20000 |
10 | 9 |
|
11 | | - // warmup (also primes twiddle cache) |
12 | | - for (let i = 0; i < warmup; i++) rfft(input) |
| 10 | +// Shared input |
| 11 | +const input = new Float32Array(N) |
| 12 | +for (let i = 0; i < N; i++) input[i] = Math.random() * 2 - 1 |
| 13 | + |
| 14 | +// --- Setup each library outside the timed loop --- |
| 15 | + |
| 16 | +// fourier-transform (this package) |
| 17 | +const ftRun = () => fft(input) |
| 18 | + |
| 19 | +// fft.js (indutny, radix-4) — widely considered fastest pure-JS FFT |
| 20 | +const FFTJS = require('fft.js') |
| 21 | +const fftjs = new FFTJS(N) |
| 22 | +const fftjsOut = fftjs.createComplexArray() |
| 23 | +const fftjsRun = () => fftjs.realTransform(fftjsOut, input) |
| 24 | + |
| 25 | +// ml-fft (in-place, pre-allocated arrays) |
| 26 | +const { FFT: MLFFT } = require('ml-fft') |
| 27 | +MLFFT.init(N) |
| 28 | +const mlRe = new Array(N), mlIm = new Array(N) |
| 29 | +const mlfftRun = () => { |
| 30 | + for (let i = 0; i < N; i++) { mlRe[i] = input[i]; mlIm[i] = 0 } |
| 31 | + MLFFT.fft(mlRe, mlIm) |
| 32 | +} |
| 33 | + |
| 34 | +// dsp.js (our ancestor, the original split-radix) |
| 35 | +const dsp = require('dsp.js') |
| 36 | +const dspfft = new dsp.FFT(N, 44100) |
| 37 | +const dspRun = () => dspfft.forward(input) |
| 38 | + |
| 39 | +// ndarray-fft |
| 40 | +const ndfft = require('ndarray-fft') |
| 41 | +const ndarray = require('ndarray') |
| 42 | +const ndRe = new Float64Array(N), ndIm = new Float64Array(N) |
| 43 | +const ndx = ndarray(ndRe), ndy = ndarray(ndIm) |
| 44 | +const ndfftRun = () => { |
| 45 | + for (let i = 0; i < N; i++) { ndRe[i] = input[i]; ndIm[i] = 0 } |
| 46 | + ndfft(1, ndx, ndy) |
| 47 | +} |
| 48 | + |
| 49 | +// ooura (port of Ooura's C FFT, radix-4) |
| 50 | +const Ooura = require('ooura') |
| 51 | +const oo = new Ooura(N, { type: 'real', radix: 4 }) |
| 52 | +const ooIn = new Float64Array(N) |
| 53 | +const ooRe = oo.scalarArrayFactory() |
| 54 | +const ooIm = oo.scalarArrayFactory() |
| 55 | +const oouraRun = () => { |
| 56 | + for (let i = 0; i < N; i++) ooIn[i] = input[i] |
| 57 | + oo.fft(ooIn.buffer, ooRe.buffer, ooIm.buffer) |
| 58 | +} |
| 59 | + |
| 60 | +// kissfft-wasm (WASM KissFFT) |
| 61 | +const { rfft: kissRfft } = await import('kissfft-wasm') |
| 62 | +const kissInput = new Float64Array(input) |
| 63 | +const kissRun = () => kissRfft(kissInput) |
| 64 | + |
| 65 | +// fft-js (naive Cooley-Tukey, educational) |
| 66 | +const fftjs2 = require('fft-js') |
| 67 | +const fftjs2Input = Array.from(input) |
| 68 | +const fftjs2Run = () => fftjs2.fft(fftjs2Input) |
| 69 | + |
| 70 | +// --- Benchmark runner --- |
| 71 | + |
| 72 | +const libs = [ |
| 73 | + ['fourier-transform', ftRun], |
| 74 | + ['fft.js (indutny)', fftjsRun], |
| 75 | + ['ml-fft', mlfftRun], |
| 76 | + ['dsp.js', dspRun], |
| 77 | + ['ndarray-fft', ndfftRun], |
| 78 | + ['ooura', oouraRun], |
| 79 | + ['kissfft-wasm', kissRun], |
| 80 | + ['fft-js', fftjs2Run], |
| 81 | +] |
| 82 | + |
| 83 | +console.log(`\nFFT benchmark — N=${N}, ${ITERATIONS} iterations\n`) |
| 84 | + |
| 85 | +const results = [] |
| 86 | + |
| 87 | +for (const [name, fn] of libs) { |
| 88 | + // warmup |
| 89 | + for (let i = 0; i < WARMUP; i++) fn() |
13 | 90 |
|
14 | 91 | const start = performance.now() |
15 | | - for (let i = 0; i < iterations; i++) rfft(input) |
| 92 | + for (let i = 0; i < ITERATIONS; i++) fn() |
16 | 93 | const elapsed = performance.now() - start |
17 | 94 |
|
18 | | - const us = (elapsed / iterations * 1000).toFixed(1) |
19 | | - console.log(`N=${String(N).padStart(5)} ${us.padStart(6)}µs/call (${iterations} iters, ${elapsed.toFixed(0)}ms)`) |
| 95 | + const us = elapsed / ITERATIONS * 1000 |
| 96 | + results.push({ name, us, elapsed }) |
| 97 | +} |
| 98 | + |
| 99 | +// Sort by speed |
| 100 | +results.sort((a, b) => a.us - b.us) |
| 101 | + |
| 102 | +const fastest = results[0].us |
| 103 | +for (const { name, us, elapsed } of results) { |
| 104 | + const ratio = us / fastest |
| 105 | + const bar = '█'.repeat(Math.min(40, Math.round(ratio * 4))) |
| 106 | + console.log( |
| 107 | + `${name.padEnd(22)} ${us.toFixed(1).padStart(7)}µs/call ${ratio === 1 ? ' ' : ('×' + ratio.toFixed(1)).padStart(6)} ${bar}` |
| 108 | + ) |
20 | 109 | } |
| 110 | +console.log() |
0 commit comments