Calling D from Ruby

We all know that Ruby is slow. This is most common (and most missing-the-point, in fact) argument other-languages people fire against Ruby. Raw language performance is usually not so important when most of the time you call external resources, such as databases, filesystem or network, and wait for them. There are times, however, when you do things that require better computational speed or memory management than Ruby can offer.

Since Ruby is written in C, it is a common practice to write C extensions. C is, however, not the most pleasant to use language in the world, especially compared to Ruby. Therefore we might want to turn to other languages that generate C-compatible code. One of them is D, which aims to achieve what C++ failed to do - create fast, low level, object-oriented C-next-gen. Long time ago Tomasz Stachewicz tried to write a system allowing to write Ruby extensions in D. This, however, was abandoned long time ago. Other people claim that they managed to run Ruby with D in both directions, but these are with version 1.9.3, so not really interesting. We can, however, run D code with help of Ruby FFI. This is how.

The example I chose was finding first prime number greater than given input. This might not be the wisest choice or most useful problem, but it consists of fair amount of calculations, so it will do to show the benefits. My “solution” is based on similar one for Rust.

The D code:

import std.stdio, std.math;

extern(C) {
	long firstPrime(long bottom) {
		auto candidate = bottom;
		while(true) {
			candidate += 1;

			if(candidate % 2 == 0)
				continue;

			auto should_return = true;
			for(long i = 3; i < sqrt(real(candidate)); i += 2) {
				if (candidate % i == 0) {
					should_return = false;
					break;
				}
			}

			if(should_return)
				return candidate;
		}
	}
}

Note that you have to wrap your publicly exposed functions with extern(C), but that’s about it. Now we need to create a dynamic library that FFI could use. To do it I followed this guide from D website. You call this:

$ dmd -c prime.d -fPIC                                                       
$ dmd -ofprime.so prime.o -shared -defaultlib=libphobos2.so -L-rpath=/usr/lib

Of course you may have to change /usr/lib to other path where your libphobos2.so is. And prime.d to proper name of your source file. Then you tell FFI to use this file.

require 'ffi'

module DPrime
	extend FFI::Library

	ffi_lib './prime.so'
	attach_function :firstPrime, [ :long ], :long
end

puts DPrime.firstPrime(1001)

And this is it – you should see the result. So, is it really worth it? Well, to check this out I created similar code in Ruby (it wasn’t that easy because of lack of proper for loop, but I hope I avoided any unnecessary overhead in implementation):

def first_prime(n)
	candidate = n
	loop do
		candidate += 1

		next if candidate % 2 == 0

		should_skip = false

		x = 1
		root = Math.sqrt(candidate).floor
		loop do
			x += 2
			break if x > root

			if candidate % x == 0
				should_skip = true
				break
			end
		end

		next if should_skip

		return candidate
	end
end


NUM = 100003
TIMES = 100000

require 'benchmark'

Benchmark.bmbm do |x|
	x.report("ruby") { TIMES.times { first_prime(NUM) } }
	x.report("d")    { TIMES.times { DPrime.firstPrime(NUM) } }
end

The result is this:

Rehearsal ----------------------------------------
ruby   2.110000   0.000000   2.110000 (  2.113908)
d      0.240000   0.000000   0.240000 (  0.240340)
------------------------------- total: 2.350000sec

           user     system      total        real
ruby   2.110000   0.000000   2.110000 (  2.112403)
d      0.240000   0.000000   0.240000 (  0.241806)

So it seems that the D variant is almost 10 times faster for the data provided. I played a bit with input but this ratio seemed to be almost constant. I think this proved it was worth the effort. And even though writing similar code in C would not be such nightmare, D is really much more pleasant in more complex situations (it has GC, yay! it has classes, another yay!). So even though we cannot get full duplex compatibility between Ruby and D (for now) (turns out you can, see followup post) It might be a subject to thought to write some computational-heavy parts of your Ruby application in faster and modern language (be it D or Rust).

Related posts