code wiki / (root) / _fft256_q15_throughput_bench.nx

_fft256_q15_throughput_bench.nx source

↩ module page · 210 lines · 8153 B

1// _fft256_q15_throughput_bench.nx -- 256-point Q15 FFT throughput. 2// 3// THE primary W1 (automotive sensor / leak detection) workload 4// baseline per WORKLOAD_TARGETS.md. Acoustic leak detection, brake 5// pressure analysis, battery thermal classification, motor vibration 6// FFT-fingerprinting -- every one of these starts with a real-input 7// FFT in the 256..2048-point range. 8// 9// Q15 fixed-point (16-bit signed in -1.0..+1.0 range): the industry 10// default for low-power DSP per ARM CMSIS-DSP, TI C5000, and 11// every automotive sensor MCU. NishiLang has no native fp, so Q15 12// is also a natural fit for the substrate. 13// 14// Algorithm: Cooley-Tukey radix-2 decimation-in-time (DIT). In-place 15// 256-point complex FFT. Twiddle factors generated iteratively at 16// startup via the angle-addition formula: 17// cos(t+dt) = cos(t)*cos(dt) - sin(t)*sin(dt) 18// sin(t+dt) = sin(t)*cos(dt) + cos(t)*sin(dt) 19// so we hard-code ONE step (cos/sin of 2*pi/256) and grow the table. 20// 21// Bench: 1000 iterations of 256-point FFT. Reports FFTs/sec + 22// per-FFT latency in microseconds. Reference targets: 23// ARM CMSIS-DSP (Cortex-M4 @ 100 MHz, SIMD) ~50K FFTs/sec 24// FFTW double precision (modern x86_64) ~100K FFTs/sec 25// Pure-software (this bench) ~1-10K FFTs/sec est 26// CUDA cuFFT batched ~10M FFTs/sec 27// 28// Silicon-feedback consumption: rv64im_min_hot_report.nx attribution 29// over this bench identifies which functions dominate (likely the 30// butterfly inner loop's multiply + accumulate). PROP_MAC_FUSED 31// silicon candidate from rv64im_min_hot_report.nx (3000 gates, 32// 2x speedup) is the prime addition target -- THIS bench is the 33// evidence base for that decision. 34// 35// Status: SEED. 2026-05-26. Single-stage radix-2; future commits 36// add split-radix (~40% fewer multiplies) + the SIMD-vector emit 37// variant when V-ext silicon candidate lands. 38 39import "nx_syscalls.nx" 40 41const NX_FFT_N: i64 = 256 42const NX_FFT_LOG2_N: i64 = 8 43const NX_FFT_Q15_ONE: i64 = 32767 44const NX_FFT_ITERS: i64 = 1000 45 46// Twiddle step per Q15. cos(2*pi/256) ~ 0.99969 -> 32757; sin -> 804. 47const NX_FFT_COS_STEP: i64 = 32757 48const NX_FFT_SIN_STEP: i64 = 804 49 50// ===== Signed arithmetic right shift (NishiLang >> is logical only) ================================================= 51// 52// FFT Q15 multiplies produce 32-bit signed intermediate; we shift 53// right by 15 to renormalise. Logical shift wrong-signs negatives; 54// fill top bits with the sign on the negative path. 55 56func nx_fft_sar15(v: i64) -> i64 { 57 if v >= 0 { return v >> 15 } 58 // Negative: shift logically, then OR in sign-extension bits. 59 let logical: i64 = v >> 15 60 let sext: i64 = (0 - 1) << 49 // top 15 bits set 61 return logical | sext 62} 63 64// ===== Twiddle table generation ================================================= 65// 66// Builds cos_tab + sin_tab for k = 0..N/2-1 via the angle-addition 67// recurrence. ~256 Q15 multiplies; runs once per benchmark process. 68 69func nx_fft_gen_twiddles(cos_tab: *i64, sin_tab: *i64) -> i64 { 70 cos_tab[0] = NX_FFT_Q15_ONE 71 sin_tab[0] = 0 72 var k: i64 = 1 73 let half: i64 = NX_FFT_N >> 1 74 while k < half { 75 let prev_cos: i64 = cos_tab[k - 1] 76 let prev_sin: i64 = sin_tab[k - 1] 77 cos_tab[k] = nx_fft_sar15((prev_cos * NX_FFT_COS_STEP) - (prev_sin * NX_FFT_SIN_STEP)) 78 sin_tab[k] = nx_fft_sar15((prev_sin * NX_FFT_COS_STEP) + (prev_cos * NX_FFT_SIN_STEP)) 79 k = k + 1 80 } 81 return 0 82} 83 84// ===== 8-bit bit reverse ================================================= 85// 86// FFT decimation-in-time requires bit-reverse permutation. For 87// N=256 the index is 8 bits; reverse via the standard bit-swap chain. 88 89func nx_fft_bit_reverse_8(x: i64) -> i64 { 90 var v: i64 = x & 0xff 91 // Swap adjacent pairs of 4 bits, then 2 bits, then 1 bit. 92 v = ((v & 0xf0) >> 4) | ((v & 0x0f) << 4) 93 v = ((v & 0xcc) >> 2) | ((v & 0x33) << 2) 94 v = ((v & 0xaa) >> 1) | ((v & 0x55) << 1) 95 return v & 0xff 96} 97 98// ===== Bit-reverse permutation ================================================= 99// 100// In-place; only swap when reversed index > current (each pair 101// touched exactly once). 102 103func nx_fft_bit_reverse_permute(re: *i64, im: *i64) -> i64 { 104 var i: i64 = 0 105 while i < NX_FFT_N { 106 let j: i64 = nx_fft_bit_reverse_8(i) 107 if j > i { 108 let tr: i64 = re[i] 109 re[i] = re[j] 110 re[j] = tr 111 let ti: i64 = im[i] 112 im[i] = im[j] 113 im[j] = ti 114 } 115 i = i + 1 116 } 117 return 0 118} 119 120// ===== Cooley-Tukey radix-2 DIT FFT ================================================= 121// 122// Eight butterfly stages for N=256. Each stage doubles the group 123// size m; the twiddle stride is N/m so larger groups use closer-to- 124// k=0 twiddle values. 125 126func nx_fft_radix2(re: *i64, im: *i64, cos_tab: *i64, sin_tab: *i64) -> i64 { 127 nx_fft_bit_reverse_permute(re, im) 128 var stage: i64 = 1 129 while stage <= NX_FFT_LOG2_N { 130 let m: i64 = 1 << stage 131 let half_m: i64 = m >> 1 132 let step: i64 = NX_FFT_N / m 133 var group: i64 = 0 134 while group < NX_FFT_N { 135 var k: i64 = 0 136 while k < half_m { 137 let t_idx: i64 = k * step 138 let wc: i64 = cos_tab[t_idx] 139 let ws: i64 = sin_tab[t_idx] 140 let a_re: i64 = re[group + k] 141 let a_im: i64 = im[group + k] 142 let b_re: i64 = re[group + k + half_m] 143 let b_im: i64 = im[group + k + half_m] 144 // t = w * b (complex mul); Q15 * Q15 -> Q30, shift right 15. 145 let t_re: i64 = nx_fft_sar15((wc * b_re) - (ws * b_im)) 146 let t_im: i64 = nx_fft_sar15((wc * b_im) + (ws * b_re)) 147 // Butterfly 148 re[group + k] = a_re + t_re 149 im[group + k] = a_im + t_im 150 re[group + k + half_m] = a_re - t_re 151 im[group + k + half_m] = a_im - t_im 152 k = k + 1 153 } 154 group = group + m 155 } 156 stage = stage + 1 157 } 158 return 0 159} 160 161// ===== Bench main ================================================= 162 163func main() -> i64 { 164 // Allocate i64-per-bin arrays for re + im (NishiLang has no i32 165 // narrow type; the Q15 values fit comfortably in i64). 166 let re: *i64 = (sys_mmap(8 * NX_FFT_N)) as *i64 167 let im: *i64 = (sys_mmap(8 * NX_FFT_N)) as *i64 168 let cos_tab: *i64 = (sys_mmap(8 * (NX_FFT_N >> 1))) as *i64 169 let sin_tab: *i64 = (sys_mmap(8 * (NX_FFT_N >> 1))) as *i64 170 171 // Generate twiddle tables once. 172 nx_fft_gen_twiddles(cos_tab, sin_tab) 173 174 // ----- main loop ----- 175 var iter: i64 = 0 176 while iter < NX_FFT_ITERS { 177 // Reseed input each iteration so the compiler can't const-fold 178 // the FFT away. Real signal = sin wave at bin 5 + DC offset. 179 // 5/256 of full circle per sample = 7.03 degrees. Hard-code 180 // a tiny sin via 7 LUT entries indexed by i mod 36 (~10 deg 181 // resolution); accuracy doesn't matter for a perf bench. 182 var i: i64 = 0 183 while i < NX_FFT_N { 184 // Pseudo-sinusoid: tile the index 0..7 against a small LUT 185 // baked here so the bench is purely deterministic. 186 let phase: i64 = (i * 5) & 0x7 187 var sample: i64 = 0 188 if phase == 0 { sample = 0 } 189 if phase == 1 { sample = 11585 } 190 if phase == 2 { sample = 23170 } 191 if phase == 3 { sample = 30273 } 192 if phase == 4 { sample = 32767 } 193 if phase == 5 { sample = 30273 } 194 if phase == 6 { sample = 23170 } 195 if phase == 7 { sample = 11585 } 196 // Add tiny DC offset; iteration-varying so compiler 197 // can't lift sample loop out of the iter loop. 198 re[i] = sample + (iter & 0xff) 199 im[i] = 0 200 i = i + 1 201 } 202 203 nx_fft_radix2(re, im, cos_tab, sin_tab) 204 205 iter = iter + 1 206 } 207 208 // Defeat DCE: fold a byte from re[0] into the exit code (masked). 209 return (re[0] as i64) & 0 210}