はじめに
この記事は昨日の記事の続編です。前回はCrystalで書いたコードをWebAssemblyにして、wasmer や wasmtimeで実行しました。
今回は、Crystalで書いた関数をWebAssemblyに出力し、Rubyから呼び出して使ってみます。
前回のおさらい
CrystalはLLVMの機能を利用して、WebAssemblyを出力できる。
その際、コンパイル済みのwasmのライブラリを入手してリンクする必要がある。
コンパイル済みライブラリを以下のリポジトリのリリースから入手する。(本記事作成時点では 0.0.2 が最新)
CUI操作でダウンロードする場合以下のコマンド
wget https://github.com/lbguilherme/wasm-libs/releases/download/0.0.2/wasm32-wasi-libs.tar.gz
mkdir wasm32-wasi-libs
tar -xvf wasm32-wasi-libs.tar.gz -C wasm32-wasi-libs
rm wasm32-wasi-libs.tar.gz
環境変数 CRYSTAL_LIBRARY_PATH
を指定してビルド
export CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs
crystal build hello.cr -o hello.wasm --target wasm32-wasi
wasmtime hello.wasm
wasmer hello.wasm
Crystalでフィボナッチ数を求める関数を用意する
フィボナッチ数とは、こういう感じで数がだんだん増えていく数列です。
$F_1 = F_2 = 1,\ F_{n + 2} = F_n + F_{n+1}\ (n \ge 1)$
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, ...
実装はCrystal公式ブログの記事から拝借しました。(Crystal公式ブログの記事は本記事の内容とは無関係ですが一読の価値があります。)wasmに関数として書き出すためには、型を完全に指定した上で def
ではなくfun
で関数を宣言します。
# エクスポートする関数は型を完全に指定して `fun` で宣言する
fun fib(n : Int32) : Int32
if n <= 1
1
else
fib(n - 1) + fib(n - 2)
end
end
wasmにビルドします。(ビルドには wasm32-wasi-libs
が必要です。入手していない場合は上の「前回のおさらい」参照)
CRYSTAL_LIBRARY_PATH=$PWD/wasm32-wasi-libs \
crystal build fib.cr \
-o fib.wasm \
--target wasm32-wasi \
--link-flags="--export fib"
ビルドしたら、実際に関数が呼び出せるか確認してみましょう。wasmtime
コマンドでは、 --invoke
コマンドをつけることで、簡単に関数を呼び出すことができます。またバージョン3以上の wasmer
でも同様のことができます。
wasmtime run --invoke fib fib.wasm 5 # 8
wasmer run --invoke fib fib.wasm 6 # 13
うまく呼び出せませたか?
Rubyからwasmの関数を呼び出してみよう
Rubyからwasmを呼び出す方法は今後いろいろ出てくるのかもしれませんが、現状では wasmer-ruby が最も有力だと思います。wasmerのバインディングです。
gem install wasmer
それでは、Rubyから fib.wasm
を呼び出すコードを作成してみましょう。ここでの注意は、fib.wasm
は WASI を利用しているため、WASIを手動で公開しなければならいことです。なので、READMEのチュートリアルにあるコードではなく、example ディレクトリ内の wasi.rb を参考にします。
require 'wasmer'
MAX = ARGV[0].to_i
wasm_bytes = IO.read('fib.wasm', mode: 'rb')
store = Wasmer::Store.new
module_ = Wasmer::Module.new store, wasm_bytes
wasi_version = Wasmer::Wasi.get_version module_, true
wasi_env = Wasmer::Wasi::StateBuilder
.new('wasi_test_program')
.argument('--test')
.environment('COLOR', 'true')
.environment('APP_SHOULD_LOG', 'false')
.map_directory('the_host_current_dir', '.')
.finalize
import_object = wasi_env.generate_import_object store, wasi_version
instance = Wasmer::Instance.new module_, import_object
wasm_func = instance.exports.fib
10回関数を呼び出すことにします。
10.times do |i|
puts wasm_func.call(i)
end
これを実行すると、
1
1
2
3
5
8
13
21
34
55
フィボナッチ数列が表示されれば成功です!(最初は n=0 が実行されるていることに注意)
ベンチマーク
せっかくなので、簡単なベンチマークを取ってみましょう。
- Rubyで素朴にフィボナッチ数を計算する場合
- Rubyから mwasmer-ruby で fib.wasm の関数を呼ぶ場合
の比較をします。(筆者はベンチマークの測定の方法に詳しくなく苦手としているので、もっときちんとしたベンチマークができる場合は、コメント欄に実行結果を書き込んで教えてください。)ここでは、v0droさんの作成した、benchmark-plot を使って直感的にベンチマークを取っていきます。
先ほどのコードのうち、1〜10番目のフィボナッチ数列を表示するコードを取り除き、ベンチマーク用のコードを追加しました。(途中トップレベルのインスタンス変数とかが入っていてアレですが見逃してください)
require 'wasmer'
require 'benchmark/plot'
MAX = ARGV[0].to_i
wasm_bytes = IO.read('fib.wasm', mode: 'rb')
store = Wasmer::Store.new
module_ = Wasmer::Module.new store, wasm_bytes
wasi_version = Wasmer::Wasi.get_version module_, true
wasi_env = Wasmer::Wasi::StateBuilder
.new('wasi_test_program')
.argument('--test')
.environment('COLOR', 'true')
.environment('APP_SHOULD_LOG', 'false')
.map_directory('the_host_current_dir', '.')
.finalize
import_object = wasi_env.generate_import_object store, wasi_version
instance = Wasmer::Instance.new module_, import_object
@wasm_func = instance.exports.fib
# wasm でフィボナッチ数を計算するメソッド
def fib_wasm(n)
@wasm_func.call(n)
end
# ruby でフィボナッチ数を計算するメソッド
def fib(n)
if n <= 1
1
else
fib(n - 1) + fib(n - 2)
end
end
# ベンチマーク用のコード
n = (1..MAX).to_a
fib_wasm(1) # ウォームアップのため1回実行しておく
Benchmark.plot(n) do |x|
x.report('fib_wasm') do |i|
fib_wasm(i)
end
x.report('fib_ruby') do |i|
fib(i)
end
end
実行してみましょう。事前に wasm のフィボナッチ関数で、事前に45まで正しい数値が得られることを確認しています。
ruby bench.rb 45
例えば、n が 40 のときで、こんな感じです。11.7倍つまり、10倍強ぐらいの速度が出るようです。
user system total real
fib_wasm 0.389907 0.000521 0.390428 ( 0.390452)
fib_ruby 4.562256 0.000447 4.562703 ( 4.562934)
wasmの方が速いのですが、正直な感想を言うと「こんなものか」という感じもあります。C拡張ならおそらくもっとスピードが出るでしょう。
速度よりも、プラットフォームを超えて利用できるという点の方が魅力的かもしれません。
もう少し拡大して見てみましょう n が 1〜20 のとき
さらに拡大します。 n が 1〜10 のとき
こんな感じで、n=1 のときかなり遅くなるので、実行順序を逆にしてみると、
こんな風になるため、wasmを呼ぶ時は最初のうちは時間がかかり、あとから早くなることがあるようです。本当にきちんと計測するならウォームアップ的な処理をしっかりやる必要があるでしょう(今回のベンチマークでは、計測前に1回呼び出しているのみ)また、n = 1〜4 ではRubyのフィボナッチ関数を呼ぶ方がwasmを呼び出すときよりも早いことがわかります。wasmを呼び出すときに一定のコストがかかるのでしょう。
まとめと感想
この記事は、Crystal言語で記述したフィボナッチ数を求める関数をWebAssemblyに出力して、Rubyから呼び出しました。
手元のベンチマークでは、実行速度はRubyで素朴にフィボナッチ数を求めるメソッドを書くよりも10倍ほど速いという結果が得られました。
Crystal言語は非常に高速ですが、現状ではCrystal言語で記述されたメソッドをほかの言語から呼び出せないという課題をかかえています。そのため、Ruby拡張をCrystal言語で記述するなど、確実に有用性が見込めるようなケースでも、なかなか使用することができませんでした。しかしながら、このようにCrystal言語でWasmを出力し、Rubyで利用できるならば、ネイティブ拡張ほどでなくても、Crystalの速度をRubyで享受することが可能になると思われます。
この記事は以上です。
参考資料