Ruby GemsRustNative として利用可能になった のでとりあえず UUIDv4 を生成してみた。

リポジトリ

katsyoshi/rust_uuid - GitHub

準備

Ruby 側の gem に Rust を利用する準備として rb_sysrake-compiler を利用します。この二つの gem は native compile するためにインストールしておきます。 Rust 側から Ruby へ関数を公開するために rb-sysmagnus を利用します。

gem install

とりあえず cargo で Rust のパッケージを作って Rust を書いてみます。

> bundle gem rust_uuid --mit --ext rust_uuid # --ext を指定してnative build する gem を作成
> cd rust_uuid # 作成した gem のディレクトリへ移動
> cd ext/rust_uuid # ビルドするディレクトリへ移動
> cargo init . --lib # cargo を初期化
> rm -f *.c *.h # C のファイルが生成されるので削除
> cargo add rb-sys rb-allocator
> cargo add magnus --features rb-sys-interop
> cargo add uuid --features v4 # uuid v4 を指定

ext/rust_uuid/extconf.rb を以下のように編集します。

@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
 require "mkmf"
+require "rb_sys/mkmf"
 
-create_makefile("rust_uuid/rust_uuid")
+create_rust_makefile("rust_uuid/rust_uuid")

次に ext/rust_uuid/src/lib.rs を以下の様に変更します。

use magnus::{define_module, function, prelude::*, Error};
use rb_allocator::ruby_global_allocator;
use uuid::Uuid;

ruby_global_allocator!();

// UUIDv4 を文字列として公開
fn v4() -> String {
    Uuid::new_v4().to_string()
}

#[magnus::init]
fn init() -> Result<(), Error> {
    let module = define_module("RustUUID")?;
    // RustUUID.v4 と利用するようにシングルトンメソッドを定義
    module.define_singleton_method("v4", function!(v4, 0))?;
    Ok(())
}

これまでできたら一旦 Rust をコンパイルしましょう。

> cd ext/rust_uuid
> cargo build
> git add .
> rake build
.... # install cargo dependencies and packages
cp: '/home/katsyoshi/Program/Ruby/rust_uuid/tmp/x86_64-linux/rust_uuid/3.1.2/target/release/librust_uuid.so'stat できません: そのようなファイルやディレクトリはありません
gmake: *** [Makefile:551: foo_bar.so] エラー 1
rake aborted!
Command failed with status (2): [/usr/bin/gmake...]

Tasks: TOP => build => compile => compile:x86_64-linux => compile:foo_bar:x86_64-linux => copy:rust_uuid:x86_64-linux:3.1.2 => tmp/x86_64-linux/rust_uuid/3.1.2/rust_uuid.so

とエラーになります。 これは ext/rust_uuid/Cargo.toml の設定が足りていません。そこで以下を追加してみてください。

[lib]
crate-type = ["cdylib"]

追加したら gem をビルド&&インストール&&試してみましょう!

> rake install
....
> ruby -rrust_uuid -e 'puts RustUUID.v4'
2be6f4d2-200b-4d08-9a1a-11fa523b316b

べんちまーく

以下、 SecureRandom.uuid との比較用のベンチマークコードを示します。

require "benchmark/ips"
require "securerandom"
require "rust_uuid"

Benchmark.ips do |x|
  x.report("standard") { SecureRandom.uuid }
  x.report("rust lib") { RustUUID.v4 }
  x.compare!
end

結果発表〜

Rust を利用することでだいたい 5 倍ほど速くなっています。

> ruby bentimark.rb
Warming up --------------------------------------
            standard    36.437k i/100ms
            rust lib   177.585k i/100ms
Calculating -------------------------------------
            standard    365.407k (± 1.4%) i/s -      1.858M in   5.086491s
            rust lib      1.793M (± 1.8%) i/s -      9.057M in   5.053179s

Comparison:
            rust lib:  1792925.9 i/s
            standard:   365407.3 i/s - 4.91x  (± 0.00) slower

ruby bentimark.rb  9.88s user 4.30s system 99% cpu 14.175 total

TOO HAYAI

まとめ

簡単に Rust を利用して速くしてみました。 思った以上に速くなっていたので重い処理をする場合に CC++ 以外でも簡単に利用できるようになって 選択肢が増えたのはよいことでした。

実はこの uuid crate の features に fast-rng を追加すると 10 倍速くなるんですが、 ruby 側の終了時に SEGV してしまうので載せていないです。 SEGV しないように原因を調査などはまた今度。