|
1 | 1 | /* eslint-disable no-console */ |
| 2 | +import FFT from 'fft.js'; |
| 3 | +import { XSadd } from 'ml-xsadd'; |
| 4 | + |
2 | 5 | import { reimFFT } from '../src/reim/reimFFT.ts'; |
3 | 6 | import { reimArrayFFT } from '../src/reimArray/reimArrayFFT.ts'; |
| 7 | +import type { DataReIm } from '../src/types/index.ts'; |
4 | 8 |
|
5 | | -const size = 2 ** 16; |
6 | | -const count = 10; // number of spectra in the array benchmark |
| 9 | +const size = 2 ** 16; // 64k-point transform: FFT setup dominates the cost |
| 10 | +const count = 10; // number of spectra processed per round |
| 11 | +const targetMs = 5000; |
7 | 12 |
|
8 | | -// Build input data |
| 13 | +// Deterministic, reproducible input so every section runs on identical data. |
| 14 | +const { random } = new XSadd(42); |
9 | 15 | const spectra = Array.from({ length: count }, () => { |
10 | 16 | const re = new Float64Array(size); |
11 | 17 | const im = new Float64Array(size); |
12 | 18 | for (let i = 0; i < size; i++) { |
13 | | - re[i] = Math.random(); |
14 | | - im[i] = Math.random(); |
| 19 | + re[i] = random(); |
| 20 | + im[i] = random(); |
15 | 21 | } |
16 | 22 | return { re, im }; |
17 | 23 | }); |
18 | 24 |
|
19 | | -// Warmup |
20 | | -for (const s of spectra) reimFFT(s); |
21 | | -for (const s of spectra) reimFFT(s, { inPlace: true }); |
22 | | -reimArrayFFT(spectra); |
23 | | -reimArrayFFT(spectra, { inPlace: true }); |
24 | | - |
25 | | -const targetMs = 5000; |
| 25 | +/** |
| 26 | + * `reimFFT` as it was *before* the cache fix: a fresh `FFT` instance is built on |
| 27 | + * every call. Kept here as the baseline to confirm the cached version is faster. |
| 28 | + * @param data - complex spectrum. |
| 29 | + * @returns FFT of the complex spectrum. |
| 30 | + */ |
| 31 | +function reimFFTNoCache(data: DataReIm): DataReIm<Float64Array> { |
| 32 | + const { re, im } = data; |
| 33 | + const length = re.length; |
| 34 | + const csize = length << 1; |
26 | 35 |
|
27 | | -// --- reimFFT (loop over each spectrum individually) --- |
28 | | -{ |
29 | | - let iterations = 0; |
30 | | - const start = performance.now(); |
31 | | - console.time('reimFFT (loop)'); |
32 | | - while (performance.now() - start < targetMs) { |
33 | | - for (const s of spectra) reimFFT(s); |
34 | | - iterations++; |
| 36 | + const complexArray = new Float64Array(csize); |
| 37 | + for (let i = 0; i < csize; i += 2) { |
| 38 | + complexArray[i] = re[i >>> 1]; |
| 39 | + complexArray[i + 1] = im[i >>> 1]; |
35 | 40 | } |
36 | | - const elapsed = performance.now() - start; |
37 | | - console.timeEnd('reimFFT (loop)'); |
38 | | - console.log( |
39 | | - ` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`, |
40 | | - ); |
41 | | - console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`); |
42 | | -} |
43 | 41 |
|
44 | | -console.log(''); |
| 42 | + const fft = new FFT(length); |
| 43 | + const output = new Float64Array(csize); |
| 44 | + fft.transform(output, complexArray); |
45 | 45 |
|
46 | | -// --- reimFFT inPlace (loop over each spectrum individually) --- |
47 | | -{ |
48 | | - let iterations = 0; |
49 | | - const start = performance.now(); |
50 | | - console.time('reimFFT inPlace (loop)'); |
51 | | - while (performance.now() - start < targetMs) { |
52 | | - for (const s of spectra) reimFFT(s, { inPlace: true }); |
53 | | - iterations++; |
| 46 | + const newRe = new Float64Array(length); |
| 47 | + const newIm = new Float64Array(length); |
| 48 | + for (let i = 0; i < csize; i += 2) { |
| 49 | + newRe[i >>> 1] = output[i]; |
| 50 | + newIm[i >>> 1] = output[i + 1]; |
54 | 51 | } |
55 | | - const elapsed = performance.now() - start; |
56 | | - console.timeEnd('reimFFT inPlace (loop)'); |
57 | | - console.log( |
58 | | - ` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`, |
59 | | - ); |
60 | | - console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`); |
| 52 | + return { re: newRe, im: newIm }; |
61 | 53 | } |
62 | 54 |
|
63 | | -console.log(''); |
64 | | - |
65 | | -// --- reimArrayFFT (single call for the whole array) --- |
66 | | -{ |
67 | | - let iterations = 0; |
| 55 | +/** |
| 56 | + * Run `task` repeatedly for `targetMs` and report the time per FFT. Each round |
| 57 | + * performs `count` transforms. |
| 58 | + * @param label - section name. |
| 59 | + * @param task - one round of work (transforms all `count` spectra). |
| 60 | + */ |
| 61 | +function bench(label: string, task: () => void): void { |
| 62 | + task(); // warmup |
| 63 | + let rounds = 0; |
68 | 64 | const start = performance.now(); |
69 | | - console.time('reimArrayFFT'); |
70 | 65 | while (performance.now() - start < targetMs) { |
71 | | - reimArrayFFT(spectra); |
72 | | - iterations++; |
| 66 | + task(); |
| 67 | + rounds++; |
73 | 68 | } |
74 | 69 | const elapsed = performance.now() - start; |
75 | | - console.timeEnd('reimArrayFFT'); |
| 70 | + const totalFFTs = rounds * count; |
| 71 | + console.log(label); |
76 | 72 | console.log( |
77 | | - ` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`, |
| 73 | + ` ${(elapsed / totalFFTs).toFixed(3)} ms per FFT (${totalFFTs} FFTs over ${rounds} rounds)`, |
78 | 74 | ); |
79 | | - console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`); |
| 75 | + console.log(''); |
80 | 76 | } |
81 | 77 |
|
| 78 | +console.log(`FFT size: ${size} (2^16), ${count} spectra per round`); |
82 | 79 | console.log(''); |
83 | 80 |
|
84 | | -// --- reimArrayFFT inPlace (single call for the whole array) --- |
85 | | -{ |
86 | | - let iterations = 0; |
87 | | - const start = performance.now(); |
88 | | - console.time('reimArrayFFT inPlace'); |
89 | | - while (performance.now() - start < targetMs) { |
90 | | - reimArrayFFT(spectra, { inPlace: true }); |
91 | | - iterations++; |
92 | | - } |
93 | | - const elapsed = performance.now() - start; |
94 | | - console.timeEnd('reimArrayFFT inPlace'); |
95 | | - console.log( |
96 | | - ` ${iterations * count} total FFTs, ${count} spectra × ${iterations} rounds`, |
97 | | - ); |
98 | | - console.log(` ${(elapsed / (iterations * count)).toFixed(3)} ms per FFT`); |
99 | | -} |
| 81 | +// Before the fix: new FFT instance per call. |
| 82 | +bench('reimFFT — before fix (new FFT per call)', () => { |
| 83 | + for (const spectrum of spectra) reimFFTNoCache(spectrum); |
| 84 | +}); |
| 85 | + |
| 86 | +// After the fix: FFT instance cached per size and reused across calls. |
| 87 | +bench('reimFFT — after fix (cached FFT instance)', () => { |
| 88 | + for (const spectrum of spectra) reimFFT(spectrum); |
| 89 | +}); |
| 90 | + |
| 91 | +// reimArrayFFT reuses a single FFT instance (and working buffers) for the whole |
| 92 | +// array in one call. |
| 93 | +bench('reimArrayFFT (single shared FFT instance)', () => { |
| 94 | + reimArrayFFT(spectra); |
| 95 | +}); |
0 commit comments