Skip to content

Benchmarks

How fast is doppler, and is it getting faster? These are single-machine numbers measured by hand for each release, not in CI. Each release is measured in two builds: portable is the PyPI wheel, and native is built from source for this CPU (builds). The trend under each number covers every release measured on this machine.

v0.81.0 — measured 2026-11-08 on AMD Ryzen AI 9 465 w/ Radeon 880M, doppler 0bb4824d0, cc (GCC) 16.2.1 20260810, 7.2.10-1-cachyos; fastest core class only (cpus 0-3,10-13).

At a glance

benchmark portable native %Δ
FFT, complex float, 4,096 points 664 MSa/s 837 MSa/s +26%
Power spectrum, 1,024 bins (Welch) 538 MSa/s 699 MSa/s +30%
FIR filter, 15 real taps 397 MSa/s 1.14 GSa/s +188%
Down-converter, decimate by 2 349 MSa/s 325 MSa/s −7.0%
Arbitrary resampler, ratio 1.0001 103 MSa/s 122 MSa/s +19%
Costas carrier loop 420 MSa/s 378 MSa/s −10.0%
Delay-locked loop (code tracking) 207 MSa/s 236 MSa/s +14%
Symbol timing recovery 177 MSa/s 176 MSa/s −0.1%
M-PSK receiver, 4 samples per symbol 61.7 MSa/s 61.7 MSa/s +0.1%
Reed-Solomon decode, 8 errors 91.8 Mbit/s 91.0 Mbit/s −0.8%
int16 to float, from Python 17.9 GSa/s 33.9 GSa/s +89%
DSSS acquisition, 1,023-chip code, no Doppler search 120 MSa/s 162 MSa/s +35%

Every benchmark, by family

884 benchmarks, grouped by family. Click a row for its chart across releases, its unit and how to reproduce it.

Notes and definitions

Headline kernels

  1. FFT, complex float, 4,096 points. Forward transforms of 4,096 complex float samples (cf32), out of place; throughput counts input samples, and one transform consumes 4,096. fft::execute_cf32[n=4096]
  2. Power spectrum, 1,024 bins (Welch). Welch power-spectral-density accumulation (window, 1,024-point FFT, magnitude squared, average) over a 65,536-sample block; throughput counts input samples. psd::accumulate[nfft=1024]
  3. FIR filter, 15 real taps. A 15-tap real-coefficient FIR filter over complex input, 65,536 samples per round; throughput counts input samples. fir::execute[taps=15,real]
  4. Down-converter, decimate by 2. Digital down-converter: mix to baseband, filter and decimate by 2; throughput counts input samples. ddc::execute[rate=0.50]
  5. Arbitrary resampler, ratio 1.0001. Polyphase resampler at a ratio of 1.0001, fed in blocks of 20,480 samples; throughput counts input samples. resamp::execute[rate=1.0001,block=20480]
  6. Costas carrier loop. A Costas loop tracking a BPSK carrier: phase detector, loop filter and NCO, per input sample. costas::steps
  7. Delay-locked loop (code tracking). An early-prompt-late delay-locked loop tracking a spreading code, per input sample. dll::steps
  8. Symbol timing recovery. A Gardner-style symbol synchronizer with an interpolating resampler, per input sample. symsync::steps
  9. M-PSK receiver, 4 samples per symbol. The full M-PSK receive chain (timing, carrier, decisions) at 4 samples per symbol with the AGC off, per input sample. mpsk_receiver::steps[sps=4,agc=0]
  10. Reed-Solomon decode, 8 errors. CCSDS Reed-Solomon (255,223) decode, each codeword carrying 8 symbol errors; coded data, so it reads in bits per second: one codeword is 255 bytes, 2,040 bits. rs::decode[e=8]
  11. int16 to float, from Python. Converting a block of 65,536 real int16 values to float through the Python binding, so the wheel's call overhead is included. i16_to_f32::test_bench_steps_64k
  12. DSSS acquisition, 1,023-chip code, no Doppler search. A code-phase search for a 1,023-chip code at 3.069 Mcps and 2 samples per chip, with no Doppler search (one frequency window), pushed one 90-epoch non-coherent dwell (184,140 samples) at a time; throughput counts searched input samples. correlator::native_nc90

Terms

Builds. portable is the shipped wheel; native is -DDOPPLER_NATIVE=ON (-march=native), built from source for this CPU. The two are measured interleaved, alternating runs, keeping each benchmark's best.

%Δ. How much faster the native build is than the wheel on the same benchmark: portable time ÷ native time − 1. Kernels that vectorize gain the most; call-overhead-bound rows sit near 0%.

Since last. Portable speed against the previous release that measured the row. Changes under 3% are grey: repeat runs of an unchanged tree move about that much. Across a break the two are different quantities, and it reads break.

Trend. One point per release, portable build, drawn as speed (higher is faster). A gap is a release that did not measure the row, or measured it faster than 100 GSa/s: a hollow benchmark, not a fast kernel. A dashed line is a break: a release where the row's value changed meaning, so the line breaks rather than draw a jump that is not real. A C row breaks where its recorded unit changed; a release from before C rows recorded one (before v0.73.0) has only its round to go on, so there a round that changed size breaks. A Python row breaks where the rate it records changed, per call one release and per bit the next.

Units. Every row that processes a stream reads in samples per second: input samples consumed, whatever the row's own unit. An FFT bin, a one-sample step() and an input sample count 1; an input pair counts 2; a frame counts its nfft; a Python call over a 64k block counts 65,536. Rows whose input is not samples read in their own rate: symbols, bits or codewords per second. Setup and control calls (create, reset, retune, configure) and read-outs (a spectrum read once per display refresh, a window built once and cached) are not stream rates and read as time per call. The count comes from the row's label, else its recorded unit, else its name -- a frame's nfft, a 64k block, a one-sample step -- and a count read from the name says inferred.

Headroom. Throughput ÷ your sample rate, for stream rows. ×1 is exactly real time on this machine, portable build, on one core unless the row says otherwise (a _t4 row fans across 4 threads): green is ×1.5 or more, amber between ×1 and ×1.5, red falls behind. A receiver is a chain: its headroom is set by its slowest stage, and the stages share the core.

Row IDs. The small monospace line under each name is the benchmark's own ID: what make bench BENCH_ARGS="--c-only <component>" (C) or make bench-python (Python) reports it as.