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¶
- 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] - 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] - 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] - 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] - 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] - Costas carrier loop. A Costas loop tracking a BPSK carrier: phase detector, loop filter and NCO, per input sample.
costas::steps - Delay-locked loop (code tracking). An early-prompt-late delay-locked loop tracking a spreading code, per input sample.
dll::steps - Symbol timing recovery. A Gardner-style symbol synchronizer with an interpolating resampler, per input sample.
symsync::steps - 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] - 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] - 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 - 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.