A Weekend With RISC-V
TL;DR
- We ran Opteryx on an 8-core RISC-V board (Orange Pi RV2, 3.7GB RAM) for no particularly good reason, and it works.
- RISC-V vector intrinsics we'd written months ago had never compiled on real hardware. Porting them to the ratified RVV 1.0 spec also surfaced a latent correctness bug.
- A different toolchain found missing
#includeheaders that Apple's C++ stdlib had been silently supplying for years. Every fix helps every platform. - 190 regression tests pass at 100%. The results are about 20× slower than the latest Apple Silicon — expected, given the gap in hardware and a pile of optimisations that don't know this architecture exists yet.
How this started
A little while ago the DuckDB folks published a lovely post — RISC-V meets RDBMS — about getting their engine onto the architecture everyone keeps saying is "the future." We read it the way you read someone else's travel blog: huh, neat, I wonder if we could…
Opteryx day-to-day lives in two places: laptops (Apple Silicon, ARM) and production (x86, Cloud Run). RISC-V has never been on the menu. But we had a quiet afternoon trying to cool down after a heatwave, an 8-core RISC-V board sitting on a desk — an Orange Pi RV2, 3.7GB of RAM, no swap, Debian — and a simple two-part question:
- Does it even run?
- If it runs, how badly does it embarrass itself next to the machines we actually use?
That's the whole experiment. No grand plan.
The setup tax
The first hour was the boring kind of yak-shaving. The board had a system Python, but our build pins a specific free-threaded CPython (3.14t — we lean on the no-GIL build for parallel execution). No prebuilt binaries exist for it on RISC-V, so we built CPython from source on the board. That alone took the better part of an hour of the little cores quietly chugging. Rust and a modern GCC were already there, which saved us.
Lesson zero: on RISC-V, “just install it” is rarely just installing it. A surprising amount of the ecosystem assumes someone, somewhere, published a wheel. For this architecture, frequently nobody has. Opteryx is zero-dependency at runtime, but getting there still meant building CPython — and a few other things — from scratch. You become very familiar with the inside of a ./configure script.
SIMD code that had never met a real compiler
Here's the fun part. We'd written RISC-V vector (RVV) versions of a few hot kernels — base64, hex encode/decode, a couple of string and hashing routines — months ago. Aspirationally. They'd literally never been compiled by anything. They were Schrödinger's SIMD: simultaneously "supported" and "completely untested" until someone opened the box.
We opened the box. They did not compile.
It turned out they were written against the old, pre-ratification RVV intrinsics — the naming convention from the early toolchains. The modern, ratified RVV 1.0 spec (which today's GCC implements) renamed essentially everything with a __riscv_ prefix and changed how segmented loads and stores work — from "hand me some pointers and I'll fill them in" to proper tuple types you assemble and unpack. So this wasn't a find-and-replace; it was a genuine little port.
Two moments stood out:
A latent correctness bug. The hex decoder marks invalid characters with the value 255 and then does a "max across the lane" check to detect them. The old code used a signed max. On an unsigned byte, 255 is −1 — the smallest possible value — so the very sentinel meant to scream "invalid!" would have been quietly ignored. The correct unsigned max makes it work. It had never run, so the bug had never had a chance to be wrong out loud. Broken but honest beats green but fake — but this was the inverse: code that looked fine precisely because it had never been asked to prove itself.
Masked operations needed the "keep the untouched lanes as they were" policy variant, which takes an extra operand. The old API baked that in implicitly; the new one makes you ask for it. Get it wrong and the compiler hands you a type error that, once you decode it, is actually telling you something true about how the hardware works.
Once ported, we ran the vector kernels against plain reference implementations on inputs big enough to actually exercise the vector paths — and they agreed, byte for byte, on well-trodden silicon. That was a genuinely satisfying green.
"Builds on my Mac"
The next category of breakage was perplexing at first. Several files used std::min, std::terminate, std::vector — and didn't include the headers that define them. They'd compiled fine on our Macs for years.
Why? Apple's C++ standard library pulls a lot of those headers in transitively, so you get away with it. GCC's standard library is stricter and doesn't. So a perfectly "working" codebase produced a tidy little stack of "'vector' does not name a template type" errors the moment it met a different toolchain.
This is the unglamorous, real value of porting to a new platform: it's a free, brutally honest code review. RISC-V didn't find RISC-V bugs. It found latent portability bugs that x86 and ARM had been polite enough to hide. Every one of those fixes makes the Mac and x86 builds more correct too.
The board fought back
A couple of detours that were purely about the hardware being small:
The first was memory. We kicked off a full parallel build and the machine started dropping our SSH sessions. Several of our heavier C++ translation units want a gigabyte-plus each to compile, and eight of them at once on a 3.7GB box with no swap is a recipe for the kernel quietly executing things. Added swap, dialled back the parallelism, and it sailed through. Obvious in hindsight; mildly mysterious at midnight.
One translation unit — our Rust compute crate — took 39 minutes to compile. We went and had a coffee, and a slice of cake, and then watched it finish.
And one glorious red herring: a run that finished its work in 17 seconds and then hung for 14 minutes before the process actually exited. We went deep diagnosing a teardown deadlock... and then it never reproduced. A cold-start gremlin, gone as mysteriously as it arrived. We have chosen to make peace with it.
Does it run? How does it compare?
It builds. It imports. It runs queries — joins, window functions, group-bys, the works. Our standard lightning regression battery (190 query-shape tests) passes 100%, zero failures, and it does so repeatably — five runs, all within a few hundred milliseconds of each other. The engine is doing exactly what it does everywhere else.
As for the comparison: the same suite that's near-instant on an Apple Silicon laptop takes about twenty seconds on the little RISC-V board. There's a comfortable order-of-magnitude between a $5,000 laptop and a board that costs about as much as a trip to the cinema. We were not expecting to win a drag race against an M-series chip.
But that gap is the expected gap — clock speeds, memory bandwidth, a young toolchain, and a pile of vector and micro-optimisations that simply haven't been taught about this architecture yet. The interesting result isn't the number. The interesting result is that the number is boring: same answers, same correctness, same behaviour, just slower. Which is exactly what you want from a port.
What we took away
- A new architecture is the best code reviewer you'll ever hire. It works for free and has no feelings to spare. The portability bugs it surfaced were real, and the fixes help every platform.
- "Supported" and "tested" are different words for a reason. Our RVV kernels were "supported" for months and wrong — or at least uncompilable — the entire time. The only way to know is to run it.
- The RISC-V experience in 2026 is "bring a compiler." The hardware is here and capable; the prebuilt-binary ecosystem is still catching up. If your project can build from source cleanly, you're most of the way there.
The DuckDB post was right to be cheerful about it. It's genuinely fun. Grab a board and find out what your "builds everywhere" code actually assumes.
— Justin