LoginSignup
12
2

More than 1 year has passed since last update.

CrystalでWebAssemblyに出力した関数をRubyから呼び出す

Last updated at Posted at 2022-12-01

はじめに

この記事は昨日の記事の続編です。前回は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 もしくは wasmer を用いる。

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 で関数を宣言します。

fib.cr
# エクスポートする関数は型を完全に指定して `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 を参考にします。

fib.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番目のフィボナッチ数列を表示するコードを取り除き、ベンチマーク用のコードを追加しました。(途中トップレベルのインスタンス変数とかが入っていてアレですが見逃してください)

bench.rb
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

benchmark_plot_graph.png

例えば、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 のとき

benchmark_plot_graph.png

さらに拡大します。 n が 1〜10 のとき

benchmark_plot_graph.png

こんな感じで、n=1 のときかなり遅くなるので、実行順序を逆にしてみると、

benchmark_plot_graph.png

こんな風になるため、wasmを呼ぶ時は最初のうちは時間がかかり、あとから早くなることがあるようです。本当にきちんと計測するならウォームアップ的な処理をしっかりやる必要があるでしょう(今回のベンチマークでは、計測前に1回呼び出しているのみ)また、n = 1〜4 ではRubyのフィボナッチ関数を呼ぶ方がwasmを呼び出すときよりも早いことがわかります。wasmを呼び出すときに一定のコストがかかるのでしょう。

まとめと感想

この記事は、Crystal言語で記述したフィボナッチ数を求める関数をWebAssemblyに出力して、Rubyから呼び出しました。
手元のベンチマークでは、実行速度はRubyで素朴にフィボナッチ数を求めるメソッドを書くよりも10倍ほど速いという結果が得られました。

Crystal言語は非常に高速ですが、現状ではCrystal言語で記述されたメソッドをほかの言語から呼び出せないという課題をかかえています。そのため、Ruby拡張をCrystal言語で記述するなど、確実に有用性が見込めるようなケースでも、なかなか使用することができませんでした。しかしながら、このようにCrystal言語でWasmを出力し、Rubyで利用できるならば、ネイティブ拡張ほどでなくても、Crystalの速度をRubyで享受することが可能になると思われます。

この記事は以上です。

参考資料

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2