induwara.lk
Opinionrustperformanceopen-source

A SIMD Viterbi decoder in Rust: reading the benchmarks properly

A SIMD Viterbi decoder in Rust beats libfec on every code in its README. But the honest speedup depends entirely on which libfec build you compare against.

Induwara Ashinsana5 min read
GitHub repository page for a Rust forward error correction crate showing README benchmarks
Image: GitHub

A SIMD Viterbi decoder in Rust landed on Hacker News this week, and the benchmark table in its README is more interesting than the headline number. The project is brian-armstrong/fec, a BSD-3-Clause forward error correction crate by the same author who wrote libcorrect in C in 2016.

I want to talk about it for a reason that has nothing to do with satellites. The way this project handles CPU capability at runtime is a pattern most of us should be copying, and almost nobody in the average web or backend stack is.


🔍 The benchmark table says two different things

Here is what the README reports, measured on a Zen4 laptop:

Code fec (64-bit) libfec (32-bit) libfec (64-bit)
conv, rate 1/2, k=7 158 Mbps 148 Mbps 17 Mbps
conv, rate 1/2, k=9 66 Mbps 65 Mbps 3 Mbps
conv, rate 1/3, k=9 61 Mbps 23 Mbps 2 Mbps
conv, rate 1/6, k=15 1187 Kbps 415 Kbps 40 Kbps
RS (255,223), general 568 Mbps 123 Mbps 157 Mbps
RS (255,223), CCSDS 568 Mbps 198 Mbps 223 Mbps
RS (255,223), general, 2 err 445 Mbps 108 Mbps 150 Mbps
RS (255,223), CCSDS, 2 err 443 Mbps 165 Mbps 207 Mbps

Compare against libfec 64-bit and rate-1/2 k=7 looks like a 9x win (158 vs 17 Mbps). Compare against libfec 32-bit and the same row is 158 vs 148, roughly a 7% win.

Both numbers are in the same table, and the crate is genuinely ahead in every row. But the size of the lead swings wildly:

  • rate 1/2, k=7 — 1.07x over the best libfec build
  • rate 1/3, k=9 — 2.7x
  • rate 1/6, k=15 — 2.9x
  • RS (255,223) CCSDS — 2.5x over libfec's faster build

Key takeaway: A benchmark is only as honest as its baseline. The most impressive column here (libfec 64-bit convolutional, 17 Mbps down to 40 Kbps) is almost certainly a baseline with its hand-written SIMD path disabled. The 32-bit column is the number a serious engineer should quote.

Notice that libfec's 64-bit build is faster than its 32-bit build for Reed-Solomon (157 vs 123 Mbps) and catastrophically slower for convolutional codes (17 vs 148 Mbps). That asymmetry is a strong hint that the old C library's convolutional decoder only had its vectorised path wired up on one target. I can't verify that from the README alone, so treat it as a reading, not a fact.


⚡ Runtime dispatch is the part worth stealing

The author's own description of the work is the useful bit: a generic, templated decoder that picks its implementation at runtime based on which instruction sets the CPU actually has. For small rates and orders, the whole decode stays in registers. Larger codes fall through to memory with acceleration structures.

The README says SIMD acceleration rides on nightly Rust, enabled through a simd feature flag, covering:

Instruction set Vector width Typical availability
SSE 128-bit Effectively every x86-64 CPU
AVX2 256-bit Most desktop/server chips from the last decade
AVX512 512-bit Newer server and high-end desktop parts

I did not see ARM NEON named in the README, so if you are targeting a Raspberry Pi or an Apple Silicon Mac, check before you assume.

The pattern matters more than the crate. Most people solve "which CPU am I on" by either:

  1. Compiling for the lowest common denominator and leaving performance on the floor, or
  2. Compiling with -march=native and shipping a binary that crashes on someone else's machine.

Runtime dispatch is the third option, and it is the correct one for anything you distribute. You compile every path, and you choose at startup.


🛰️ Why this is relevant if you are building in Sri Lanka

Forward error correction sounds like a space-agency problem. It is not.

  • Amateur radio and SDR. An RTL-SDR dongle plus a laptop is a complete satellite receive station. Decoding NOAA or cubesat downlinks is exactly what the CCSDS (255,223) code in this crate exists for.
  • LoRa and long-range telemetry. Anyone wiring up agricultural sensors, flood gauges, or remote monitoring over a marginal link is fighting bit errors, not bandwidth.
  • Anything over a bad connection. Mobile data quality varies enormously outside Colombo. Codes that trade throughput for resilience are the right engineering call more often than people assume.

The soft-decision decoder in this crate accepts 8-bit confidence symbols from the demodulator instead of hard 1s and 0s, which the README notes gives better error correction than the hard-decision variant. If you have confidence data and you are throwing it away, you are leaving correction capability unused.

The BSD-3-Clause licence also matters for a small team here. It is permissive: use it commercially, ship it in a product, no copyleft obligation. That is not true of every library in this space.


🛠️ The career lesson hiding in a hobby project

The author wrote a C library in 2016 and, years later, rewrote it in Rust rather than doing a mechanical port. Along the way they built a separate tool to search for optimal maximum-d_free convolutional codes for a given rate and order.

That is the shape of the most valuable side project you can do:

  1. Pick something you already understand deeply. The domain knowledge is the moat, not the language.
  2. Change one hard variable. Here it was the vectorisation model, not the algorithm.
  3. Build the auxiliary tool the problem demands. A code-search tool is not the deliverable, but it is what makes the deliverable credible.
  4. Publish the benchmarks with the baseline visible. Including the column that makes your win look smaller.

The API is small enough to read in one sitting:

let mut enc = ConvEncoder::new(2, 7, &[0o161, 0o127]);
let num_bits = enc.encode(msg, &mut encoded).unwrap();
dec.decode_hard(&encoded, num_bits, &mut recovered).unwrap();

Those octal values are the generator polynomials. If that line means nothing to you yet, that gap is the thing worth closing.


💡 What this means for you

You do not need a GPU or a cloud budget to do real performance engineering. This entire project is CPU work, on a laptop, against a decades-old C baseline, in a language whose portable SIMD support is still on nightly. Every ingredient is free.

Three concrete next steps:

  • If you want to try the Rust snippets without installing a toolchain, our online Rust compiler runs in the browser.
  • If you are learning why error-correcting codes work at all, the minimum distance between valid codewords is the whole idea. Our Hamming distance calculator is a reasonable place to build the intuition by hand.
  • If you maintain anything performance-sensitive, go find where you hardcoded a CPU assumption at compile time and ask whether it should be a runtime decision instead.

The speedup is nice. The dispatch pattern is the transferable part.

#rust#performance#open-source
IA

Induwara Ashinsana

Information Systems student at UCSC and Executive Director at Ryzera Technologies. Writes about software, AI, and what it means for builders in Sri Lanka.

About the author →

Keep reading