Skip to content

perf: pad inner FFT to a smooth size to remove sampling-frequency regression#78

Merged
zsoerenm merged 5 commits into
masterfrom
two-bit-acquisition
Jul 23, 2026
Merged

perf: pad inner FFT to a smooth size to remove sampling-frequency regression#78
zsoerenm merged 5 commits into
masterfrom
two-bit-acquisition

Conversation

@zsoerenm

Copy link
Copy Markdown
Member

Summary

Removes the sampling-frequency performance regression documented in Sampling Frequency and FFT Performance, by zero-padding the inner double-block correlation FFT up to the next FFTW-fast (2·3·5·7-smooth) length.

The inner FFT ran at exactly 2 × block_size, a size fixed by the sampling frequency (block_size = samples_per_code ÷ num_blocks). When samples_per_code carries a large prime factor, that lands on a slow FFTW radix. The flagship case is 16.368 MHz (= 16 × 1.023 MHz, the canonical GNSS IF): block_size 1023 → 2046 = 2·3·11·31, a radix-31 transform ~5× slower than a nearby smooth size.

plan_acquire now sets fft_size = nextprod((2, 3, 5, 7), 2 × block_size) — the same max_prime = 7 "fast regime" that recommend_sampling_freqs already uses. No-op when the natural size is already smooth.

Results are unchanged

Any length ≥ 2 × block_size preserves the first block_size linear-correlation lags (no aliasing into the zero tail), and the inverse FFT is normalised by the actual length, so the recovered correlation is mathematically identical. Verified end-to-end at 16.368 / 8.184 MHz, padded vs. forced-unpadded, on the same input:

Quantity Δ (padded vs unpadded)
Correlation power surface < 1e-6 relative (Float32 rounding floor)
Doppler 0 (bit-identical)
Code phase 0 (bit-identical)
CN0 ~2e-6 dB

Measured speedup

acquire!(GPSL1CA(), signal, 1:32), 24 threads:

Sampling freq inner FFT size before after
16.368 MHz 2046 → 2048 20.1 ms 4.2 ms (~5×)
8.184 MHz 744 → 750 12.7 ms 4.8 ms (~2.6×)
16.384 MHz (smooth control) 2048 (no-op) 3.6 ms 3.6 ms

Padded 16.368 MHz (4.2 ms) now matches the smooth 16.384 MHz rate.

Scope / limitations

Padding fixes only the inner FFT. A large prime in num_blocks (hence num_doppler_bins) still slows the column FFT — a genuine DFT over the Doppler axis that cannot be zero-padded without changing its bins (e.g. 1.542 MHz → num_blocks = 257). Those rates still benefit from recommend_sampling_freqs. The doc section is updated to reflect this.

Testing

  • Full test suite passes (incl. Aqua).
  • Correctness and timing verified as above.

🤖 Generated with Claude Code

…ression

The inner double-block correlation FFT ran at exactly 2*block_size, a size
fixed by the sampling frequency (block_size = samples_per_code / num_blocks).
When samples_per_code carries a large prime factor the inner FFT lands on a
slow FFTW radix — e.g. 16.368 MHz (= 16 x 1.023 MHz, the canonical GNSS IF)
gives block_size 1023 -> 2046 = 2*3*11*31, a radix-31 transform ~5x slower
than a nearby smooth size.

plan_acquire now zero-pads the inner FFT up to the next 2*3*5*7-smooth length
(fft_size = nextprod((2,3,5,7), 2*block_size)), the same max_prime=7 "fast
regime" recommend_sampling_freqs already uses. This is a no-op when the natural
size is already smooth.

Results are unchanged: any length >= 2*block_size preserves the first
block_size linear-correlation lags, and the inverse FFT is normalised by the
actual length, so the recovered correlation is mathematically identical.
Verified end-to-end at 16.368/8.184 MHz vs the unpadded path: power surface
matches to <1e-6 relative (Float32 rounding floor), Doppler and code phase
bit-identical. Measured acquire!(1:32): 16.368 MHz 20.1 ms -> 4.2 ms, matching
the smooth 16.384 MHz rate.

Column-FFT-bound rates (large prime in num_blocks, e.g. 1.542 MHz -> 257) are
unaffected: a DFT cannot be zero-padded losslessly. Full test suite passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.18%. Comparing base (0b993ad) to head (9876f2f).

Additional details and impacted files
@@           Coverage Diff           @@
##           master      #78   +/-   ##
=======================================
  Coverage   97.17%   97.18%           
=======================================
  Files          12       12           
  Lines        1170     1172    +2     
=======================================
+ Hits         1137     1139    +2     
  Misses         33       33           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Inner-FFT padding removes the inner-FFT half of the sampling-frequency
regression, so the old guidance (prefer a smooth samples_per_code for the inner
FFT, the 16.368 MHz radix-31 "offender", the Fast/Slow inner-FFT timing table)
is obsolete. Rewrite the "Sampling Frequency and FFT Performance" section to
explain that the inner FFT is now auto-padded (bit-identical results) and that
the remaining sensitivity is the column FFT (num_blocks / num_doppler_bins with
a large prime, which a DFT cannot be zero-padded to avoid, e.g. 1.542 MHz -> 257).

Also update the two spots that referenced the old framing: the index.md tip
(16.368 MHz is no longer slow) and the num_blocks section's now-moot
"second-order effect" paragraph. recommend_sampling_freqs docs are unchanged
(still useful for the column FFT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master 9876f2f... master / 9876f2f...
Acquire/2.048MHz/10ms/1prns 3.06 ± 0.0087 ms 3.17 ± 0.012 ms 0.966 ± 0.0045
Acquire/2.048MHz/10ms/32prns 0.0454 ± 0.0011 s 0.0477 ± 0.00084 s 0.951 ± 0.029
Acquire/2.048MHz/1ms/1prns 0.26 ± 0.0064 ms 0.259 ± 0.0065 ms 1 ± 0.035
Acquire/2.048MHz/1ms/32prns 3.98 ± 0.061 ms 4.27 ± 0.1 ms 0.931 ± 0.027
Acquire/2.048MHz/20ms/1prns 7.38 ± 0.017 ms 7.11 ± 0.022 ms 1.04 ± 0.004
Acquire/2.048MHz/20ms/32prns 0.0954 ± 0.001 s 0.1 ± 0.0013 s 0.954 ± 0.016
Acquire/2.048MHz/5ms/1prns 1.4 ± 0.0078 ms 1.4 ± 0.0083 ms 0.997 ± 0.0081
Acquire/2.048MHz/5ms/32prns 21.3 ± 0.14 ms 22.5 ± 0.21 ms 0.945 ± 0.011
Acquire/5MHz/10ms/1prns 6.89 ± 0.023 ms 7.76 ± 0.026 ms 0.888 ± 0.0042
Acquire/5MHz/10ms/32prns 0.102 ± 0.00079 s 0.107 ± 0.0006 s 0.958 ± 0.0092
Acquire/5MHz/1ms/1prns 0.568 ± 0.0075 ms 0.657 ± 0.0073 ms 0.864 ± 0.015
Acquire/5MHz/1ms/32prns 8.26 ± 0.039 ms 8.87 ± 0.052 ms 0.932 ± 0.007
Acquire/5MHz/20ms/1prns 13.5 ± 0.032 ms 15.6 ± 0.035 ms 0.868 ± 0.0028
Acquire/5MHz/20ms/32prns 0.202 ± 0.0018 s 0.214 ± 0.00017 s 0.942 ± 0.0085
Acquire/5MHz/5ms/1prns 3.62 ± 0.011 ms 4.12 ± 0.012 ms 0.88 ± 0.0038
Acquire/5MHz/5ms/32prns 0.0548 ± 0.00058 s 0.0575 ± 0.00026 s 0.953 ± 0.011
AcquireSignals/E1B_15MHz 14.6 ± 0.33 ms 14.6 ± 0.23 ms 0.998 ± 0.027
AcquireSignals/L1CA_16.368MHz 4.23 ± 0.026 ms 1.45 ± 0.016 ms 2.91 ± 0.037
AcquireSignals/L1CA_5MHz 0.551 ± 0.0099 ms 0.553 ± 0.0099 ms 0.996 ± 0.025
AcquireSignals/L1CA_5MHz_Nnc8 4.15 ± 0.034 ms 4.17 ± 0.027 ms 0.994 ± 0.01
AcquireSignals/L1CP_16MHz 0.0988 ± 0.0014 s 0.103 ± 0.0016 s 0.964 ± 0.021
AcquireSignals/L5I_12MHz_Ncoh10 0.0864 ± 0.00041 s 0.0816 ± 0.00059 s 1.06 ± 0.0092
AcquireSignals/L5I_25MHz 3.21 ± 0.042 ms 3.23 ± 0.032 ms 0.996 ± 0.016
BitEdgeSearch/N_be=1 7.35 ± 0.018 ms 7.2 ± 0.02 ms 1.02 ± 0.0037
BitEdgeSearch/N_be=10 0.0492 ± 4.9e-05 s 0.051 ± 5.3e-05 s 0.965 ± 0.0014
BitEdgeSearch/N_be=2 14.7 ± 0.03 ms 14.3 ± 0.027 ms 1.02 ± 0.0029
BitEdgeSearch/N_be=5 27.6 ± 0.045 ms 27.5 ± 0.037 ms 1 ± 0.0021
LongCoherent/10ms 3.04 ± 0.0087 ms 3.18 ± 0.011 ms 0.956 ± 0.0043
LongCoherent/1ms 0.257 ± 0.0064 ms 0.256 ± 0.0065 ms 1.01 ± 0.036
LongCoherent/20ms 7.38 ± 0.02 ms 6.98 ± 0.018 ms 1.06 ± 0.004
LongCoherent/40ms 28.3 ± 0.09 ms 26.4 ± 0.042 ms 1.07 ± 0.0038
LongCoherent/5ms 1.37 ± 0.0079 ms 1.4 ± 0.0081 ms 0.981 ± 0.008
NonCoherent/M=1 0.259 ± 0.0064 ms 0.258 ± 0.0065 ms 1.01 ± 0.035
NonCoherent/M=10 2.19 ± 0.028 ms 2.2 ± 0.0085 ms 0.997 ± 0.013
NonCoherent/M=5 1.12 ± 0.0043 ms 1.14 ± 0.0045 ms 0.975 ± 0.0054
PlanAcquire/E1B_15MHz 1.33 ± 0.042 ms 1.33 ± 0.041 ms 0.998 ± 0.045
PlanAcquire/L1CA_16.368MHz 1.44 ± 0.076 ms 0.497 ± 0.035 ms 2.91 ± 0.26
PlanAcquire/L1CA_5MHz 0.172 ± 0.01 ms 0.18 ± 0.011 ms 0.958 ± 0.084
PlanAcquire/L1CA_5MHz_Nnc8 0.211 ± 0.017 ms 0.221 ± 0.016 ms 0.955 ± 0.1
PlanAcquire/L1CP_16MHz 3.27 ± 0.12 ms 3.31 ± 0.23 ms 0.989 ± 0.079
PlanAcquire/L5I_12MHz_Ncoh10 0.568 ± 0.12 ms 0.572 ± 0.13 ms 0.993 ± 0.3
PlanAcquire/L5I_25MHz 0.796 ± 0.038 ms 0.811 ± 0.041 ms 0.981 ± 0.069
time_to_load 0.53 ± 0.0092 s 0.552 ± 0.0072 s 0.959 ± 0.021
Memory benchmarks
master 9876f2f... master / 9876f2f...
Acquire/2.048MHz/10ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/2.048MHz/10ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
Acquire/2.048MHz/1ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/2.048MHz/1ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
Acquire/2.048MHz/20ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/2.048MHz/20ms/32prns 3 allocs: 0.75 kB 3 allocs: 0.75 kB 1
Acquire/2.048MHz/5ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/2.048MHz/5ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
Acquire/5MHz/10ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/5MHz/10ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
Acquire/5MHz/1ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/5MHz/1ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
Acquire/5MHz/20ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/5MHz/20ms/32prns 3 allocs: 0.75 kB 3 allocs: 0.75 kB 1
Acquire/5MHz/5ms/1prns 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
Acquire/5MHz/5ms/32prns 3 allocs: 0.797 kB 3 allocs: 0.797 kB 1
AcquireSignals/E1B_15MHz 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
AcquireSignals/L1CA_16.368MHz 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
AcquireSignals/L1CA_5MHz 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
AcquireSignals/L1CA_5MHz_Nnc8 1 allocs: 0.438 kB 1 allocs: 0.438 kB 1
AcquireSignals/L1CP_16MHz 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
AcquireSignals/L5I_12MHz_Ncoh10 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
AcquireSignals/L5I_25MHz 1 allocs: 0.484 kB 1 allocs: 0.484 kB 1
BitEdgeSearch/N_be=1 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
BitEdgeSearch/N_be=10 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
BitEdgeSearch/N_be=2 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
BitEdgeSearch/N_be=5 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
LongCoherent/10ms 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
LongCoherent/1ms 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
LongCoherent/20ms 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
LongCoherent/40ms 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
LongCoherent/5ms 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
NonCoherent/M=1 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
NonCoherent/M=10 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
NonCoherent/M=5 2 allocs: 0.0625 kB 2 allocs: 0.0625 kB 1
PlanAcquire/E1B_15MHz 0.331 k allocs: 8.39 MB 0.333 k allocs: 8.39 MB 1
PlanAcquire/L1CA_16.368MHz 0.219 k allocs: 2.67 MB 0.221 k allocs: 2.68 MB 0.996
PlanAcquire/L1CA_5MHz 0.203 k allocs: 0.783 MB 0.205 k allocs: 0.783 MB 1
PlanAcquire/L1CA_5MHz_Nnc8 0.215 k allocs: 2 MB 0.217 k allocs: 2 MB 1
PlanAcquire/L1CP_16MHz 0.571 k allocs: 21.4 MB 0.573 k allocs: 21.4 MB 1
PlanAcquire/L5I_12MHz_Ncoh10 0.254 k allocs: 8.75 MB 0.256 k allocs: 8.75 MB 1
PlanAcquire/L5I_25MHz 0.235 k allocs: 3.87 MB 0.237 k allocs: 3.87 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

zsoerenm and others added 3 commits July 23, 2026 13:57
The existing suite only benchmarks smooth sampling rates (2.048/5/10/15/16/25/12
MHz — all powers of two or 2^a*5^b), where inner-FFT padding is a no-op, so it
neither shows the padding speedup nor guards its fast path. Add GPS L1CA at
16.368 MHz (samples_per_code 16368 = 2^4*3*11*31 → inner FFT lands on a large
prime), which AirspeedVelocity runs against both master (slow, unpadded radix
FFT) and this branch (padded to a 2*3*5*7-smooth size). Guards the regression
fix and makes the improvement visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rewrite dropped the old Fast/Slow timing table (it measured the now-fixed
inner FFT). Add a small table quantifying the penalty that is still real after
padding — a large prime in num_blocks / num_doppler_bins, which the column FFT
cannot be zero-padded to avoid. Measured GPS L1CA, 32 PRNs: 1.542 MHz
(num_doppler_bins = 257) is ~35x slower than the adjacent 1.500 MHz, and
3.069 MHz (31) ~3x slower than 3.000 MHz. Notes that rates slow only because of
the inner FFT (e.g. 6.138, 16.368 MHz) are no longer affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since the inner double-block FFT is now zero-padded to a 2*3*5*7-smooth size, its
factorization (and samples_per_code's) no longer affects speed — only the column
FFT (num_doppler_bins), a DFT that cannot be padded, does. recommend_sampling_freqs
was still filtering and ranking by inner-FFT / samples_per_code smoothness, which
made it wrongly reject now-fast rates (e.g. 16.368 MHz: non-smooth samples_per_code
16368 = 2^4*3*11*31, but num_doppler_bins = 16).

Changes:
- Gate candidates only on num_doppler_bins max-prime (drop the samples_per_code
  and inner-FFT gates).
- Report inner_fft_size as the padded length actually run (nextprod((2,3,5,7),
  2*block_size)); inner_max_prime is therefore <= 7 by construction.
- Rank :cost and :smoothness by num_doppler_bins_max_prime.
- Cost model uses the padded inner size.

16.368 MHz is now recommended (num_doppler_bins = 16); 1.542 MHz (forced to
num_blocks = 257) is still rejected. Tests updated + a new testset covering both;
full suite passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zsoerenm
zsoerenm merged commit 0347706 into master Jul 23, 2026
9 checks passed
@zsoerenm
zsoerenm deleted the two-bit-acquisition branch July 23, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant