なぜかRustのドキュメントの日本語版だけに英語で存在しているRust Inside Other Languagesという章に、FFIを使ってRubyの並列処理を高速化するというのがあったのでやってみたら爆速でした 🚀
この記事タイトルも雑ですが内容も雑です。説明する気はない感じになってます 🙏 雰囲気を感じていただければ 😉
元の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コアしか使えてません 😭
なので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の波が来てる!!(マイブーム)