Long story short: I wrote a Ruby extension in Zig
by Paweł Świątkowski
25 Dec 2022
I always had mixed feelings about writing native Ruby extensions. They surely are the way to make critical parts of the code run faster, but the developer experience around that was not great for a Ruby programmer like me. Sure, I know C. This was the second language I learned (after Pascal). I can write C, I can read C, and I even wrote some larger extensions in it, but I never felt comfortable with it.
Other options were not great as well. I can use Rust, but I have never been on good terms with Rust. I have a story of rewriting part of our production code as Rust extension, which turned out to be 3-4 times faster, but we ditched it anyway, because maintenance cost would be too high, I might tell it some other time. I have experimented a lot with writing extensions in D, and I actually have a working setup for that. But D has its own garbage collector and rumour has it it’s not a good mix. D also has a
@nogc mode, but it feels like giving up on 60% of the language’s power.
When Zig entered the scene, my hopes of finding another language with which I could write Ruby extensions got up again. But soon I found out it was not possible. Ruby uses many arcane C parts and its extensive usage of varargs, which was not supported by Zig’s C export, was a deal-breaker. Fortunately, a few weeks ago an issue I’ve been tracking for about 3 years was resolved. Zig now can export functions with variable arguments. So writing Ruby extensions in it should be possible.
I gave it a try.
The extension test
What I did is a really simple implementation of 100 doors problem from Rosetta Code. I took a Zig implementation from there (I’m not very fluent in Zig myself), altered it to take one argument (number of passes) and rewrote it in Ruby:
After that I wrapped it in the Ruby extension code in Zig:
Finally, I ran a benchmark against both versions:
As you can see, in my case the Zig version was almost 20 times faster. The standard deviation also seems to be much lower, so the execution speed is more predictable. I tried that on a much older machine and then the difference wasn’t as big as this, but also significant (almost 6 times faster). But that’s not all. This code is compiled by Zig in debug mode. What would happen if I compiled it with the fastest possible option (
-Drelease-fast)? Let’s take a look:
The zig version is almost 300 times faster! Of course, I suspect that, due to the fact that it is basically a simple computation in the loop, compiler did some serious magic on optimizing it and there is no chance on getting this kind of speedup in a real-life situation. But anyway, I plan to dig a little deeper into it, perhaps try it with some more realisting examples and see what I can get.
For starters though, I am really happy to see that it is at least possible to use Zig for it. We will see how it goes.
If you want to try out the example yourself, here is the complete code: https://github.com/katafrakt/zig-ruby. It likely only works on Linux, but I’d be happy to accept the PR for MacOS (I don’t think Windows support is feasible for now).