Ruby and D Interop: Part 3
In this final entry of the Ruby and D interop series I’ll be taking you through a practical usage example. We’ll look at an algorithm from the early Section 3 ARG days that does some byte manipulation to create a crude form of ‘encryption’.
From here on out I’ll be using the dub package manager that comes bundled with D to handle the build process.
The Sleuth Algo
The actual specifics of the algorithm are for another blog post but it’s enough to know that we process two input bytes from an array of bytes labeled datanode
for each byte of output. The final output is largely determined by our key bytes giving the illusion of some cryptographic function.
On my machine running this algorithm against the 8MB data node provided in the sleuth example takes about 1.3 seconds on average. This isn’t exactly terrible if you already know the correct key for the data node but if you had to brute-force to find the key… you might be here a while.
Sleuth in D
decrypt
is written in a more C-like style as I wanted to see the differences between a C and D approach. The only real difference here from a straight C implementation is the GC.calloc()
instead of calloc()
.
decrypt_p
is written in a fully D style. Even throwing in a little parallelism in there for fun. The really nice thing about this is that it’s a simple straight translation from Ruby to D.
If we want to test these functions out and make sure they’re working correctly we can take advantage of D’s built in Unit Testing.
If we append this code to the code above and run dub test
we’ll see that all of our tests pass.
I’m using the rubyffi mixin from the second part in this series to simplify the binding generation step. You can simply run ./bindgen
in the example from the repo to produce the correct bindings.
Likewise you can run dub build --build=release
to compile the sleuth example library.
Passing pointers
If we inspect the code produced by ./bindgen
we’ll see that we have
The rubyffi mixin has translated the immutable(ubyte*)
and ubyte*
types into :pointer
on the Ruby end for us.
In our Ruby application code we want to write a small helper function to deal with these pointers.
This function simply sets up the pointers we require with the correct data types and tacitly returns the result as an array of integers on the Ruby end. I feel like it should be possible to update rubyffi to generate these stubs for us.
Putting it all together
Now that we have our D decrypt function accessible to Ruby we can actually look at ‘decrypting’ the data.node
Taken directly from the sleuth example repo
Build instructions:
./bindgen
dub build --build=release
./main.rb
You should get a working MP4 out.mp4
as the output.
Was it worth it?
Well let’s run a simple ./benchmark.rb
from the repo and see what shakes out.
Rehearsal ------------------------------------------------
rb_decrypt: 1.250000 0.000000 1.250000 ( 1.356179)
d_decrypt: 0.078000 0.016000 0.094000 ( 0.101248)
d_decrypt_p: 0.110000 0.015000 0.125000 ( 0.083242)
--------------------------------------- total: 1.469000sec
user system total real
rb_decrypt: 1.313000 0.000000 1.313000 ( 1.341551)
d_decrypt: 0.094000 0.000000 0.094000 ( 0.105518)
d_decrypt_p: 0.062000 0.016000 0.078000 ( 0.091533)
It looks like the D functions execute at least thirteen times faster than their Ruby counterpart which is a pretty solid performance increase for very little work.
d_decrypt_p
just ekes out a win over the serial d_decrypt
implementation though how much it wins by (or loses) will vary depending on how many cores you have. I was quite impressed by how easy it was to parallelise the algorithm. All I had to do was drop .parallel on the output variable - almost as an after thought to see how it would work out and… work out it did!
If I had to brute-force the key to one of these data nodes I know which path I’d choose.
Series conclusion
In part 1 I provided an overview of writing D code and creating the bindings from scratch to be run by Ruby.
In part 2 we progressed to using D to generate our bindings at compile time using a library I’ve authored to expedite the process of getting up and running.
By this point I hope you have a really good view on how to start using D to make Ruby gains. I hope I’ve demonstrated that it’s not scary or hard to get down to that ‘low-level’ and optimise some piece of code that gets run often in your Ruby application. D should not be too unfamiliar territory to a Ruby developer, the UFCS will make you feel right at home!
I’m not going to say it’s a pain-free experience but this method should certainly be quicker than wading in to building native C extensions with the Ruby devkit.
I find being able to easily unit test my extension code to be a big win.
One final thought. We do have to be a bit wary of who owns the memory allocated in our D functions. We passed a pointer to D allocated memory over to Ruby… the D garbage collector has no idea that Ruby is using this value and could collect (or not) at any time causing us issues.
It doesn’t really apply to this example as we’re acquiring a pointer to fresh memory on each invocation but it’s definitely something I need to investigate further - perhaps a future blog entry.