Go SIMD Beats C in Debian Code Search: I Ran the Benchmarks
In August, Michael Stapelberg deleted the last cgo dependency from Debian Code Search — ending a seven-year marriage to the C TurboPFor library, made possible by Go's experimental SIMD package. Today he published the writeup. Receipts beat rhetoric, so I did what he did and what most people won't: I checked out the exact commit where the C benchmark still existed, compiled both paths on a machine he never touched (Intel Ice Lake, not his AMD Zen), and ran the fight myself. The encoder result: Go SIMD at 629.5 Mval/s vs 254.3 Mval/s through cgo — 2.5x faster than the C code DCS actually shipped, with byte-identical compressed output. The decoder: a statistical dead heat. Here's the full scoreboard.
Why Debian Code Search spent 7 years married to C
DCS indexes all of Debian's source code and answers literal searches (78.2% of queries) from an on-disk positional index — posting lists of document IDs compressed with TurboPFor, an integer compression format whose decoder is aggressively vectorized. Every query decodes these lists, so decode speed is query speed. The C library was fast enough that replacing it wasn't worth the risk, and the alternatives for SIMD in Go were all ugly: hand-written assembly (only feasible for tiny functions), Avo-generated assembly (still assembly), or cgo into C (which DCS did, with the C library even prebuilt into a checked-in .syso object).
Go 1.26 changed the equation in February 2026: a new experimental simd/archsimd package, enabled with GOEXPERIMENT=simd, exposing 128/256/512-bit vector types like Int8x16 and Float64x8 as ordinary Go. No assembler. No C. Stapelberg's experiment: could native Go match the C library that had been "serving us well" since 2019?
I ran the benchmarks: Intel Ice Lake vs his AMD Zen 4/5
His post benchmarks on a Ryzen 9 9950X3D (Zen 5). I cloned Debian/dcs, checked out 70fddea — the last commit before 5f4f0d5 deleted cgo on Aug 23 — and ran the repo's own Makefile targets: GOAMD64=v4 GOEXPERIMENT=simd, six counted runs per case, pinned to one core, benchstat for aggregation. This VM is a Xeon Silver 4310T (Ice Lake, 2.3 GHz) — a CPU with full AVX-512 but nothing like Zen 5's clock speed.
# dcs @ 70fddea, internal/turbopfor/pforenc
# GOAMD64=v4 GOEXPERIMENT=simd, taskset -c 1, -count=6
$ go test -run='^$' -bench='Encode/n=2048/vals=debian-mix' -benchtime=200000x -count=6
BenchmarkEncode/n=2048/vals=debian-mix/impl=c 254.3 Mval/s 2906 encoded-bytes
BenchmarkEncode/n=2048/vals=debian-mix/impl=go 629.5 Mval/s 2906 encoded-bytes (+147.6%)
The aggregate across all 36 block-type cases (encoder):
| Encoder (Mval/s) | C via cgo | Go SIMD | vs C |
|---|---|---|---|
| debian-mix, n=2048 | 254.3 ± 2% | 629.5 ± 3% | +148% |
| debian-mix, n=160 (remainder) | 214.2 ± 15% | 376.6 ± 7% | +76% |
| constant blocks, n=2048 | 329.6 | 5156.5 | 15.6x |
| geomean (all 36 cases) | 294.9 | 979.0 | +232% |
The decoder is the honest counterweight. On the real-world debian-mix at n=2048, Go and C are statistically indistinguishable (2868 vs 2747 Mval/s, p=0.18). The geomean across all decoder cases: C 2815, Go 2719 — Go 3.4% behind, i.e. a tie. The texture underneath is worth reading: C wins big on pure bitpacking full blocks (13.4k vs 9.8k Mval/s on bw2), Go wins on exception-heavy blocks (+27% on bw2-exc). On his Zen 4/5 hardware Stapelberg reported Go matching or exceeding cgo on decode; on my Intel it's a wash. Two vendors, same conclusion: the cgo path no longer buys you anything.
One more receipt most benchmarks skip: compression parity. Across every case, Go's encoder produced byte-identical output sizes to C — geomean +0.00%, every sample equal. This isn't a lossy "close enough" reimplementation; it's a drop-in format match, 0 allocs/op on both sides.
The optimization ladder, commit by commit
The best part of the repo is that the git log is the scoreboard — every optimization commit carries its own Mval/s in the message:
graph TD
subgraph Encoder
A[e4161be
trivial encoder] --> B[e920dc7
76% of C, format match]
B --> C[6a9b173
AVX2 bitpack: 743f/459r]
C --> D[2db8415
unroll per bit-width: 1023f/784r]
D --> E[d02ff36
positional popcount AVX512:
2148f/1203r]
end
subgraph Decoder
F[9c3564c
scalar Go decoder] --> G[211e23a
AVX2 remainders: 4288f/2140r]
G --> H[70fddea
unrolled SIMD: 12270f/2235r]
H --> I[5f4f0d5
cgo deleted Aug 23]
end
Three details deserve your attention. First, the biggest single encoder win — positional popcount, a 2x jump — came from Claude Fable 5 spotting that block-type selection, not encoding, had become the bottleneck. Second, Stapelberg is blunt that he is not claiming Go beats C: backport the same AVX-512 kernels and positional popcount into C TurboPFor and C pulls ahead by ≈1.4x. The comparison that matters is against the C you actually have, not the C you could theoretically write. Third, he used Claude Code for exploration but refused to "vibe-code" production — he rewrote the SIMD kernels himself, keeping the agent as a very fast pair of eyes on objdump output: "The LLM can read objdump output much faster than I can... never runs out of patience to run one more experiment, as long as I give it measurable and reachable goals."
Should you rip out cgo yet? Read the fine print first
The tempting takeaway — "cgo is dead, SIMD in Go is free" — is wrong in three specific ways. The package is experimental and its API is explicitly unstable. The sweet results require GOAMD64=v4, which means AVX-512: AMD Zen 4+, Intel Ice Lake+ — and it silently strands older CPUs unless you keep build-tagged scalar fallbacks (DCS ships cpu_nosimd.go for exactly this). And the decoder here needed ~10 optimization rounds to reach parity; the naive SIMD port was slower than C, as it always is.
But the strategic takeaway survives all three caveats. There is now a fourth option for SIMD-shaped Go work — hot loops over typed arrays, compression codecs, hashers, filters — that isn't assembly, isn't cgo, and doesn't forfeit Go's toolchain, profilers, or safety. The performance-critical middle of the Go ecosystem has kept C dylibs alive for a decade for lack of exactly this. DCS is one production system proving the exit works, with the receipts to prove nobody had to pay for it in speed.
Bottom line: I reproduced Stapelberg's headline on a different CPU vendor and it held — encoder 2.5x the shipped cgo path on the real-world workload, decoder at parity, byte-identical compressed output, zero allocations. Go's SIMD package is the first plausible end for the "hot loop? ship a C dylib" era of Go engineering. If you maintain a Go project with one cgo dependency that exists purely for speed, the window to replace it just opened. Measure before you jump — but you no longer have an excuse not to measure.