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

created at 2016/08/16 00:34:24

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

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

元のRubyコード

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

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コアしか使えてません :sob:

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

Rustで書く

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

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

爆速!!! :fire::rocket:

おわりに

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

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

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