Performance

go-fft is benchmarked head-to-head against the implementations that matter, on the same machine, with the same inputs and sizes:

  • FFTW 3.3.10, built from source with the CPU’s SIMD and called from C, the gold standard, single-threaded with a reused FFTW_MEASURE plan.
  • pocketfft via numpy.fft 2.5.3 and scipy.fft 1.18.1, single-threaded.
  • gonum/dsp/fourier v0.16.0, the other pure-Go (CGO_ENABLED=0) FFT.

Correctness is gated first: every go-fft transform must match numpy.fft within rtol=1e-9, atol=1e-7 before any timing is reported.

Correctness, not speed: v0.25.0 to v0.26.2 return wrong N-D results (FFTN, FFT2, PlanN and their inverses), silently, on Intel CPUs with AVX-512, when an axis other than the last has a length 9·p (153, 171, 207, …). Fixed in v0.26.3; use v0.26.3 or later. See the v0.26.3 release notes.

The numbers below describe v0.27.0. Each host’s numbers are the newest parity runs of the code its paths run in v0.27.0. v0.24.0 (Round 37) changed only Haswell’s and arm64’s paths. v0.25.0 (Round 35), v0.26.3 (the N-D correctness fix) and v0.27.0 (Round 39) changed only Intel AVX-512’s. v0.26.0 (Round 36) changed only paths taken where the CPU is not Intel’s. v0.26.1, v0.26.2 and v0.26.4 changed no library code. Measured on three GCC Compile Farm hosts with go1.27.1, every row on one pinned core, all on 2026-10-11:

  • Neoverse-N1: Round 40, the v0.26.1 tag (arm64 has not changed since);
  • Cascade Lake: Round 39, the branch that became v0.27.0;
  • Zen 3: Round 36, the branch that became v0.26.0.

What these runs do not contain is said under each host: Round 39’s parity binary predates its rebase onto v0.26.3, whose fix touches no row here, and Round 36’s parity was taken on the v0.23.1 base.

FFTW’s own times move by up to 47% between runs on one host, so no single run is quoted. go-fft’s time is the median of the three repetitions in each run, then the mean of the two runs named under each host; FFTW’s is the median over the round’s runs: four on Cascade Lake and Zen 3 (each of those rounds ran main, branch, branch, main), two on Neoverse-N1 (Round 40 ran the tag twice); numpy and scipy are the mean of the same two runs as go-fft. The tables are computed from the raw files by current-numbers-v0.27.0/, which also lists each run’s own value and the command that produced each table. The raw runs are in benchmarks/results/, and the dated optimization rounds (what was tried, kept or dropped, and why) are in BENCHMARKS.md. Reproduce them with benchmarks/run.sh (benchmarks/remote/ for a Linux host without Go).

Method

  • One pinned core (taskset to a single core, so GOMAXPROCS is 1), the 2-D rows included: go-fft’s multicore N-D path has one thread here, like FFTW, numpy and scipy.
  • Steady state: every library reuses its plan, and go-fft writes into a reused slice (Plan, RealPlan, and PlanN for 2-D). The 2-D ratios are PlanN against FFTW, not the allocating FFT2 call, which the per-run reports in the raw data also give, with GFLOP/s.
  • ns/op, lower is better. The ratio is go-fft ÷ FFTW (≤ 1.05 = parity, which includes rows slower than FFTW by at most 5%; every such row is named).

How go-fft transforms a length

  • Lengths whose prime factors are all ≤ 13: an iterative mixed-radix Stockham engine, with radix-8/4/2/3/5/7 passes plus a general radix-p pass. On amd64 its radix-2/3/4/5/8 passes run as AVX2 kernels, and as AVX-512 kernels for powers of two of 256 points or more. Both are bit-identical to the Go passes.
  • A prime whose N−1 is 7-smooth: Rader’s algorithm. Any other length: Bluestein’s chirp-z. Both convolve on the Stockham engine.
  • Powers of two on loong64, s390x and amd64 without AVX2, and above 65536 on riscv64: an iterative, cache-blocked radix-4 kernel.
  • On arm64 and the other non-amd64 targets the passes are Go code, compiled to scalar instructions. gc does not vectorize; it does fuse multiply-adds.

The three hosts at a glance

go-fft time ÷ FFTW time; below 1 means go-fft is faster. Bold: at or above FFTW, which by the project’s convention includes rows up to 1.05, slower than FFTW by at most 5%. “Faster” below means strictly below 1. Three decimals where two would hide which side of 1 or 1.05 a row is on.

Zen 3Neoverse-N1Cascade Lake
at or above FFTW (≤ 1.05), of 24182415
of which faster than FFTW172414
at or above numpy.fft, in both runs, of 24242424
at or above scipy.fft, in both runs, of 24242424
faster than gonum, in both runs, of 19 (1-D)191919

The 24 rows are the complex 1-D, real 1-D and 2-D tables below (11 + 8 + 5); the IRFFT rows are not counted, as in every round.

AMD EPYC 7773X (Zen 3), AVX2

cfarm420, a 128-thread VM shared with other users (1-minute load 1.9–2.3 during the parity runs); FFTW 3.3.10 built from source with SSE2/AVX/AVX2/FMA. Runs par2-br and par3-br of Round 36; FFTW over par1-main, par2-br, par3-br, par4-main (main is v0.23.1).

  • Within 5% but slower than FFTW: RFFT 1,024 (1.047).
  • Slower by more: complex 256 (1.10), 1,024 (1.13), 1,080 (1.08); RFFT 256 (1.051), 4,096 (1.09), 1,080 (1.06).
  • Not in these runs: they were taken on the v0.23.1 base, before Round 36 was rebased onto v0.25.0 (its A/Bs were redone on v0.25.0). The rebase brings Zen 3 one change of its own: v0.25.0’s tail jump makes the real-FFT untangle one call instead of two on every CPU without AVX-512, a call Round 37 measured at about 1.5% of RFFT 256 on Haswell. Round 38 re-took Zen 3’s parity at v0.26.1, with the tail jump: its RFFT rows moved by −4.2% to +7.4% in ratio and the complex rows the jump does not touch by −2.3% to +7.2%, so the jump’s effect is inside that measurement’s spread (20 of 24 within 5%, 15 faster, under loads up to 3.2). That is a cross-check only; the table stays Round 36’s.
transformgo-fftFFTWgo-fft ÷ FFTWnumpy.fftscipy.fft
complex 2563072791.106,7335,417
complex 1,0241,7601,5531.1312,8979,729
complex 4,0969,58210,7070.8944,57033,320
complex 65,536257,943297,4920.871,889,202926,177
complex 1,048,5766,008,33611,885,3130.5139,631,04513,845,480
complex 1,000 (2³·5³)2,0622,0910.98613,60110,033
complex 1,080 (2³·3³·5)2,4772,2881.0814,39910,891
complex 1,920 (2⁷·3·5)3,9184,0030.9821,65416,335
complex 1,009 (prime, Rader)12,55120,4310.6161,00538,647
complex 1,296 (2⁴·3⁴)2,7903,0510.9116,19312,495
complex 10,007 (prime, Bluestein)167,374203,8770.82644,482474,608
RFFT 2562372261.0516,0825,369
RFFT 1,0241,0099641.0479,4848,317
RFFT 4,0965,7225,2711.0924,94622,350
RFFT 65,536138,438155,6260.89416,831500,548
RFFT 1,048,5762,954,6563,768,5950.788,387,30710,298,716
RFFT 1,000 (2³·5³)1,1591,3000.8910,2018,669
RFFT 1,080 (2³·3³·5)1,4121,3311.0610,8999,123
RFFT 1,920 (2⁷·3·5)2,4232,4450.99114,33212,130
2-D 64×649,79910,9580.8948,02533,946
2-D 128×12851,95562,6220.83147,828125,980
2-D 256×256265,382295,2810.90711,655456,853
2-D 512×5121,306,9421,318,2290.9912,902,8031,870,872
2-D 1024×10246,370,5426,647,4580.9613,657,4159,738,945

Neoverse-N1 (arm64)

cfarm424, 64 cores, used by the round alone (load 0.04–1.09); FFTW 3.3.10 built from source with NEON. Runs r1 and r2 of Round 40, both the v0.26.1 tag; FFTW over the same two runs.

  • Faster than FFTW on all 24 rows. None is within 5% and still slower. The closest are RFFT 256 (0.998) and RFFT 1,080 (0.994), 1.049 and 1.039 on v0.23.1 (Round 37’s main runs): Round 37’s NEON untangle loads with VLD1 and VZIP instead of VLD2 and reads a pre-split twiddle table.
  • Since Round 37’s table (v0.24.0): Bluestein’s 10,007 went 0.72 → 0.69. v0.26.0 moved its three pointwise products to one function, on arm64 the same scalar loop behind a call, and an A/B measured v0.26.1 2% faster than v0.25.0 there. 2-D 256² went 0.50 → 0.54 with no change to its code on arm64 since v0.24.0; one binary ran it at 610–705 µs between processes (likely allocation placement, not proved). Every other row is within 0.01 of Round 37’s.
  • FFTW’s variance: its own 10,007 moved 5.9% between the two runs (586 and 620 µs).
transformgo-fftFFTWgo-fft ÷ FFTWnumpy.fftscipy.fft
complex 2561,0121,1490.889,5227,642
complex 1,0244,8996,3270.7718,80914,538
complex 4,09624,00740,9500.5960,10748,093
complex 65,536726,4671,252,1820.582,243,6761,509,643
complex 1,048,57621,166,42245,054,6650.4746,540,25931,873,243
complex 1,000 (2³·5³)6,0767,1680.8519,20415,192
complex 1,080 (2³·3³·5)6,8847,6670.9021,73316,765
complex 1,920 (2⁷·3·5)11,99413,5400.8933,02726,127
complex 1,009 (prime, Rader)21,63648,1400.4596,66357,253
complex 1,296 (2⁴·3⁴)8,75211,3700.7725,00819,610
complex 10,007 (prime, Bluestein)415,486603,2670.691,023,385760,683
RFFT 2567197210.9988,6407,854
RFFT 1,0242,9404,2040.7014,33212,564
RFFT 4,09614,31419,8640.7237,31433,231
RFFT 65,536392,310544,3610.72737,580760,281
RFFT 1,048,57610,468,75218,834,9110.5619,618,02518,495,787
RFFT 1,000 (2³·5³)3,7403,9670.9415,17012,591
RFFT 1,080 (2³·3³·5)4,1834,2070.99415,89413,925
RFFT 1,920 (2⁷·3·5)6,8007,3370.9321,74118,966
2-D 64×6424,53531,0110.7970,71250,410
2-D 128×128128,074194,9750.66259,414200,828
2-D 256×256684,2281,272,7190.541,092,138894,998
2-D 512×5123,295,9626,854,4090.486,507,5074,320,211
2-D 1024×102418,348,44044,114,8030.4227,336,93322,074,923

Intel Xeon (Cascade Lake), AVX-512

cfarm151, 8 vCPUs, used by the round alone (load 0.26–1.02 during the parity runs); FFTW 3.3.10 built from source with AVX-512 codelets. Runs p-br1 and p-br2 of Round 39; FFTW over p-main1, p-br1, p-br2, p-main2 (main is v0.26.1).

  • All five 2-D rows are faster than FFTW (0.83–0.95), and 14 of the 24 rows (10 in v0.26.0). Round 39 widened the column strips of large N-D axes and put the 2-D rows on the stack-frame kernels on Intel too.
  • Within 5% but slower than FFTW: RFFT 4,096 (1.049).
  • Slower by more: complex 256 (1.052), 1,024 (1.19), 65,536 (1.08), 1,000 (1.10), 1,080 (1.13), 1,920 (1.11), 1,296 (1.10); RFFT 65,536 (1.09) and RFFT 1,048,576 (1.09).
  • RFFT 1,048,576 is noise: one run read 7.97 ms and the other 10.49, and FFTW’s four runs read 6.6–9.3 ms. Round 39 does not touch that path (its A/B reads 1.021, 0.980–1.093).
  • FFTW’s variance: its own 2-D 1024² ran 15.9–23.3 ms over the four runs, so go-fft’s 0.83 there is against the median (0.90 against FFTW’s fastest run).
  • Not in these runs: Round 39 was rebased onto v0.26.3 after these runs. v0.26.3’s fix changes only N-D axes of length 9·p, which no row here has.
transformgo-fftFFTWgo-fft ÷ FFTWnumpy.fftscipy.fft
complex 2563893701.0528,8046,505
complex 1,0242,5372,1341.1916,35512,153
complex 4,09612,37013,1220.9460,93344,471
complex 65,536436,546405,7321.081,887,0611,291,823
complex 1,048,57618,192,57925,810,6780.7047,883,34441,127,411
complex 1,000 (2³·5³)3,0232,7501.1016,99613,965
complex 1,080 (2³·3³·5)3,4223,0301.1320,46113,235
complex 1,920 (2⁷·3·5)5,4844,9321.1131,50522,961
complex 1,009 (prime, Rader)18,03730,3620.5980,49656,581
complex 1,296 (2⁴·3⁴)3,9653,5951.1020,78916,529
complex 10,007 (prime, Bluestein)339,064360,9830.94896,852724,882
RFFT 2563433650.946,5817,075
RFFT 1,0241,1901,5150.7812,98410,385
RFFT 4,0967,8767,5061.04932,89531,569
RFFT 65,536205,210187,4921.09594,791689,933
RFFT 1,048,5769,226,6448,452,8341.0919,769,65720,641,798
RFFT 1,000 (2³·5³)1,7781,8650.9513,57511,927
RFFT 1,080 (2³·3³·5)2,0612,0720.99513,55411,673
RFFT 1,920 (2⁷·3·5)3,3973,4890.9718,15716,632
2-D 64×6415,94917,0270.9457,62245,179
2-D 128×12876,61681,0100.95204,093182,495
2-D 256×256447,658471,5700.95889,079758,109
2-D 512×5122,100,3702,379,8830.885,071,5453,569,429
2-D 1024×102414,216,23617,053,6140.8326,710,30924,662,554

Honest read

  • Where go-fft leads FFTW:
    • Neoverse-N1 on all 24 rows, composites and 2-D included (1,296: 0.77×, 2-D 128²: 0.66×), the closest being RFFT 256 (0.998) and RFFT 1,080 (0.994);
    • Zen 3 on 17 of 24: complex from 4,096 points (0.51–0.89×), the composites 1,000, 1,920 and 1,296 (0.91–0.99×), Bluestein’s 10,007 (0.82×), RFFT 1,000 and 1,920 (0.89×, 0.991×), RFFT from 65,536 (0.78–0.89×) and every 2-D row (0.83–0.99×);
    • Cascade Lake on 14 of 24: every 2-D row (0.83–0.95×), complex 4,096 and 2^20 (0.94×, 0.70×), Bluestein’s 10,007 (0.94×), and RFFT 256, 1,024, 1,000, 1,080 and 1,920 (0.78–0.995×);
    • primes on every host: Rader’s 1,009 in 0.45–0.61× FFTW’s time.
  • Where FFTW leads:
    • small powers of two on amd64: complex 256 at 1.10× on Zen 3 (1.052× on Cascade Lake), complex 1,024 at 1.13× and 1.19×; on Zen 3 RFFT 256 (1.051×) and 4,096 (1.09×). For complex 256 and 1,024 and RFFT 4,096, FFTW’s 32- and 64-point codelets need two passes. Round 38 built a bit-identical radix-32 pass for amd64; it lost to split radix 8 wherever it was timed and was not shipped (v0.26.4);
    • complex and RFFT 1,080 on Zen 3 (1.08×, 1.06×);
    • on Cascade Lake, the composites 1,000–1,920 (1.10–1.13×) and complex and RFFT 65,536 (1.08×, 1.09×).
  • Changed since the previous version of this page (v0.26.0):
    • Cascade Lake (v0.27.0): 2-D 64² 1.19 → 0.94, 128² 1.027 → 0.95, 256² 0.97 → 0.95, 512² 1.20 → 0.88, 1024² 0.80 → 0.83. Of those, 1024²’s two numbers are against FFTW medians of different rounds; against v0.26.1 in the same runs it went 1.025 → 0.834. The previous page put 512² down to allocation placement; Round 39 found it was not.
    • Neoverse-N1 (re-measured at v0.26.1): Bluestein’s 10,007 0.72 → 0.69, 2-D 256² 0.50 → 0.54.
  • Against numpy and scipy: go-fft is at or above both on every row, in every run, on all three hosts (0.04–0.68 of numpy’s time, 0.04–0.86 of scipy’s).
  • Against gonum: go-fft is faster on every row measured (19/19 on each host).
  • Not in these tables: Haswell (Intel AVX2-only) has no FFTW on its host. At v0.21.0 it runs complex 512–2^18 1.03–1.21× and the composites 1000–6000 1.07–1.21× faster than v0.19.1 (Round 29); v0.24.0 runs 2-D 64×1024 … 1024² 1.04–1.06× and complex 6,144 and 10,240 1.20× faster than v0.23.1 (Round 37). Round 37 also measured that v0.22.x cost Haswell’s RFFT 256 about 1.5%; v0.25.0’s tail jump removes the call responsible, but has not been re-measured on Haswell. Apple M4 has not been measured with the current engines, and no loong64 host could be reached.
  • Single precision: float32 takes 0.46–0.83× the float64 time on Zen 3 and 0.58–0.73× on Neoverse-N1. FFTW’s single-precision library is not built on the hosts, so there is no float32 comparison with it.