RustをRubyから呼び出したら爆速

Contents
  1. 元のRubyコード
  2. Rustで書く
  3. FFIする
  4. 実行してみる
  5. おわりに

なぜか Rust のドキュメントの日本語版だけに英語で存在している Rust Inside Other Languages という章に、FFI を使って Ruby の並列処理を高速化するというのがあったのでやってみたら爆速でした 🚀

この記事タイトルも雑ですが内容も雑です。説明する気はない感じになってます 🙏 雰囲気を感じていただければ 😉

元のRubyコード

単純に 10 スレッド立ち上げて 1 スレッド毎に 5 100 万回カウントするという処理です。

threads = []

10.times do
  threads << Thread.new do
    count = 0
    5_000_000.times do
      count += 1
    end
    count
  end
end

threads.each do |t|
  puts "Thread finished with count=#{t.value}"
end

puts "done!"

なんとこれだけでもマイ Mac Book Pro だと 2 秒以上かかりました。

$ time ruby pure_ruby.rb
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
done!
ruby pure_ruby.rb  2.52s user 0.05s system 99% cpu 2.593 total

Ruby には GIL (Global Interpreter Lock)という仕組みがあって CPU を 1 コアしか使えてません 😭

なので CPU ぶん回すような並列処理はだいぶ遅いです。 top コマンドとかで 1 コアしか使えてないのを確認できます。

Rustで書く

というわけで、並列化の部分を Rust に外出ししてみます。 Rust は所有権とかいう難しいあれのおかげで、並列処理も安全かつ高速にいい感じにやってくれます ✨

use std::thread;

#[no_mangle]
pub extern fn process() {
    let handles: Vec<_> = (0..10).map(|_| {
        thread::spawn(|| {
            let mut x = 0;
            for _ in 0..5_000_000 {
                x += 1
            }
            x
        })
    }).collect();

    for h in handles {
        println!("Thread finished with count={}",
                 h.join().map_err(|_| "Could not a join thread!").unwrap());
    }
}

詳しくはドキュメントを読んでいただければわかると思います。 とにかく Rust 側でスレッドを使って同じ処理をしています。

これをコンパイルすると、libembed.dylib というファイルができます。 これを Ruby で使えるようにします。

FFIする

Ruby から Rust の関数を呼ぶには FFI(Foreign Function Interface)という仕組みを使います。 まずは Gem をインストールします。

gem install ffi

Ruby のコードは次のようになります。

require "ffi"

module Hello
  extend FFI::Library
  ffi_lib "embed/target/release/libembed.dylib"
  attach_function :process, [], :void
end

Hello.process

puts "done!"

なんとなく感じはわかります。 ffi_lib "embed/target/release/libembed.dylib" という行で Rust をコンパイルしたライブラリを Hello モジュールに読み込んでます。 attach_function はライブラリの関数を Ruby のメソッドとして割り当てるメソッドです。 第 2 引数が関数の引数の型で、第 3 引数が返り値の型です。

実行してみる

$ time ruby embed.rb
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
done!
ruby embed.rb  0.09s user 0.05s system 98% cpu 0.144 total

爆速!!! 🔥🚀

おわりに

今仕事でパフォーマンスチューニングとかもやってるので、チャンスがあれば使ってみたいなー 😍 (無理に使おうとは思わない 😅 )

WebAssembly へコンパイルする実装とかも 未完成ながらある っぽいので、フロントエンドとかでも使えるかもしれない 💪 WebAssembly は Mozilla も絡んでるし!

Rust の波が来てる!!(マイブーム)