Rust to the rescue (of Ruby)

You are working with Ruby, probably with Rails or Sinatra. Your company has an algorithm that is becoming more complex and important everyday.

Since the user needs to see the result of this algorithm, the requirement is that it must run blazingly fast and (of course) doesn’t crash.

For the sake of this article, let’s simplify and say that your algorithm uses fibonacci of 42 aka F(42) inside to calculate the result and this is the heaviest computation.

DISCLAIMER: Fibonacci is not the point of this article at all. The goal is to illustrate how Rust can be combined with Ruby when performance is the goal. Probably standard deviation is a better example but I could not make

The Algo in Ruby

Let’s implement Fibonnaci in Ruby. It is very simple to do so:

def fibonacci(n)
return n if n <= 1
fibonacci( n - 1 ) + fibonacci( n - 2 )
end

Now let’s ask it to calculate F(42). (If you want to run this on your machine, this is the time to have some coffee)

puts fibonnaci(42)
267914296
ruby fibo_in_ruby.rb 43.38s user 0.07s system 99% cpu 43.486 total

It takes 43.38 seconds!

It is very slow to use it inside a Web application for real time calculation.

This doesn’t fit our first requirement, speed. It must run faster! How can we do that?

We could make some improvements in the Ruby code, like memoization that would solve the problem. But this is not the point of this article. Let’s try Rust.

Rust for the rescue

Let’s give Rust a try and see how much it can improve the performance of our algorithm.

Rust comes with Cargo, that is a very handy CLI. To start the project, run:

cargo new fibonacci

That command creates this structure:

fibonacci
├── Cargo.toml
├── src
│ └── lib.rs

Cargo.toml is like a mix of gemspec with Gemfile. For now it contains these lines:

[package]
name = "fibonacci"
version = "0.1.0"
authors = ["Your Name <yourmail@rusting.com>"]

Open up src/lib.rs, it comes with this:

#[test]
fn it_works() {}

The #[test] is an annotation saying that this will be executed in the test suite, so let the TDD begin!

TDD

Starting simple, F(1) should be 1.

#[test]
fn one() {
assert_eq!(1, fibonacci(1));
}

We can run the test with:

cargo test

It fails to compile because we did not define the fibonacci function in Rust yet.

fn fibonacci(n: i32) -> i32 {
1
}

This is a simple function in Rust. The function declaration says that it receives a parameter named n that is a 32-bit signed integer type (i32) and responds an integer as well.

For now it is responding hard-coded 1, that is enough to make the test pass. Let’s check it:

cargo test

And this time:

running 1 test
test one … ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

Yay! It passes!

Let’s go ahead and add some more scenarios to our test suite:

#[test]
fn one() {
assert_eq!(1, fibonacci(1));
}
#[test]
fn two() {
assert_eq!(1, fibonacci(2));
}
#[test]
fn three() {
assert_eq!(2, fibonacci(3));
}
#[test]
fn four() {
assert_eq!(3, fibonacci(4));
}
#[test]
fn five() {
assert_eq!(5, fibonacci(5));
}
#[test]
fn six() {
assert_eq!(8, fibonacci(6));
}

With this we expect some failing tests, and to make them pass:

fn fibonacci(n: i32) -> i32 {
match n {
1 => 1,
2 => 1,
_ => fibonacci(n — 1) + fibonacci(n — 2)
}
}

Run the test suite again:

cargo test
Compiling fibonacci v0.1.0
Running target/debug/fibonacci-272f566d730790ce
running 6 tests
test five ... ok
test four ... ok
test one ... ok
test six ... ok
test three ... ok
test two ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured

All green! :)

Now that we’ve got the Rust version, we need to connect it with Ruby.

Connecting Ruby with Rust

PS: This part of the article is very inspired in this chapter of the Rust book.

In order to accomplish this, we’re going to use FFI (foreign function interface).

But before that we need some small adjustments to use it as an extension. Edit the src/lib.rs to be like this:

#[no_mangle]
pub extern fn fibonacci(n: i32) -> i32 {
# ...
}

So basically we need to add the #[no_mangle] on the top of the function and pub extern in the beginning of the function declaration.

These changes are required to tell the Rust compiler to preserve the name of the function and it is going to be called from outside.

Add this to Cargo.toml:

[lib]
name = "fibonacci"
crate-type = ["dylib"]

This tells Rust to compile it as a dynamic library and not as Rust format.

Let’s build it now:

cargo build --release

This will generate some files inside target/release:

$ ls target/release
build deps examples libfibonacci.dylib native

We are interested in libfibonacci.dylib (depending on your OS this file may have another extension).

To call this library from Ruby we need the FFI gem. Install it with:

gem install ffi

Place a fibo.rb at the root of your project and add this code:

require 'ffi'
module Fibo
extend FFI::Library
ffi_lib 'target/release/libfibonacci.dylib'
attach_function :fibonacci, [:int], :int
end
puts Fibo.fibonacci(42)

This a simple Ruby module that

  • extends FFI
  • Points to the library we’ve generated with Rust.
  • attaches the Fibonnaci function from Rust to Ruby. The “[:int], :int” are the parameters it receives and what it responds, respectively.

That’s it! We can run it with:

ruby fibo.rb

It responds 267914296 as expected. It works, yay!

Benchmark

Let’s check the performance improvement:

The Rust code finishes in 0.91 seconds. Less than a second.

Rust is 4767.03% faster than Ruby in this example.

Conclusion

Of course that Rust would be faster than Ruby. This is not a language comparison.

For your user, a fast feedback matters a lot and Rust can be a good friend for Ruby when performance is the goal :)

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.