(この記事は、「Elixir or Phoenix Advent Calendar 2017」の25日目です)
前日は @tuchiroさんの 「ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する」でした。
本日は「Elixirから簡単にRustを呼び出せるRustler #1 準備編」の続きです。
** お知らせ **
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します
私もETSとFlowを交えた発表で参加します!
ストリームとデータ処理に興味ある方は、是非ともご参加下さい!
Rustler
Rustといえば、メモリ安全なネイティブコンパイラ言語です。厳格なメモリ安全チェックで有名なのですが、Rustlerではその厳格な部分がうまいことラップされており、今回ご紹介するような文字列変換ではRustの既存ライブラリを、Rustのコンパイルエラーの嵐に巻き込まれずに、そのスピードの恩恵を受けることができます。
RustlerはElixirからNIF経由で安全にRustのモジュールを呼び出すためのライブラリとmix拡張を含めたボイラープレートを作成するパッケージです。
今回は、新たに関数を追加してRustのライブラリであるクレートをElixirから呼び出してみます。
Rustのクレートとは?
『クレートは他の言語における「ライブラリ」や「パッケージ」と同じ意味です。このことからRustのパッケージマネジメントツールの名前を「Cargo」としています。』公式サイトより。
というわけで、Elixirのmix
にあたるツールがCargo。mix.exs
にあたるのが、Cargo.tomlになります。今回は、Rustの外部クレートである**unicode-jp**を使用します。その手順を見ていきましょう。
Rustlerでの設定
以下の手順で行います。
Rust側
Cargo.tomlに使用する外部クレートを追加します。
今回使用するクレートのunicode-jp
をCargo.toml
の依存関係の場所に追加します。このへんはmix.exsと同じ感覚です。mix deps.getにあたる動作は不要で、コンパイル時に読み込んでくれます。
[package]
name = "example"
version = "0.1.0"
authors = []
[lib]
name = "example"
path = "src/lib.rs"
crate-type = ["dylib"]
[dependencies]
rustler = "0.15.1"
rustler_codegen = "0.15.1"
lazy_static = "0.2"
unicode-jp = "0.3.0" # <==== 追加
rustのソースでは、クレートunicode-jpは、ソース内ではkanaというクレート名です。externとuseの設定を行います。(「unicode-jp」と「kana」で一切つながりがないのが不思議ですね)
#[macro_use] extern crate rustler;
#[macro_use] extern crate rustler_codegen;
#[macro_use] extern crate lazy_static;
extern crate kana; // <=== 追加
use rustler::{NifEnv, NifTerm, NifResult, NifEncoder};
use rustler::types::atom::NifAtom;
use kana::*; // <=== 追加
elixirの関数とrustの関数をマッピングします。
rust_half2kana
という関数を、elixirのhalf2kana
にマッピングします。タプルの2番目はアリティ(引数の数)です。
rustler_export_nifs! {
"Elixir.NifExample",
[("add", 2, add),
("half2kana", 1, rust_half2kana) # <== elixirの関数とrustの関数をマッピング
],
None
}
次は、rustの関数の実装です。
String型の変数s1に文字列を取り込んで、kana
(unicode-jp)クレートの関数であるhalf2kana
に文字列の参照を渡し、戻り値をNIF用にencodeしています。関数の型定義はHaskellとTypescriptを足したような怪獣みたいな雰囲気ですが、一切触らなくて良いので安心です。関数の内部は、Rustの文法を知らなくても、見ただけでなんとなく理解できると思います。
fn rust_half2kana<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let s1: String = try!(args[0].decode());
Ok((hira2kata(&s1)).encode(env))
}
前回のadd関数ではタプルを返していましたが、今回は文字列単体を返すようにしてみます。
Ok()の行を見ると、戻り値に型を設定していないのが、おわかりいただけるでしょうか?
Elixir側
エラー処理のコードを追加するだけです。
def half2kana(_a), do: exit(:nif_not_loaded)
一度ボイラープレートを設定が完了してしまえば、面倒な型のマッピングはほとんどありません。Rustの関数を書いてしまえば、2か所の設定でElixirから利用できるんです ! ・・・・
実行してみる
replを起動して実行してみましょう。
$ iex -S mix
iex(1)> NifExample.half2kana("エリクサーとラスト")
"エリクサーとラスト"
iex(2)>
半角文字が全角文字に変換できました。
戻り値も前回のadd関数では{:ok, 3}のタプルでしたが、今回は文字列型で帰ってきています。Rustで(atoms::ok(), 3)
を返せばElixirではタプルで戻ってくる。("文字列")
で返せば文字列(バイナリ)型で戻ってくるという優れものなのです。
パフォーマンス計測
さてパフォーマンスはどうでしょうか?簡単な半角カナ⇒全角カナ変換を100万回繰り返してみます。
実行環境
- Elixir 1.6.5 OTP 20
- Rustc 1.22.1
まずは筆者の書いたElixirの全角半角変換ライブラリmojiexで計測してみます。
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_-> Mojiex.convert("アイウエオかきくけこサシスセソたちつてと",{:hk,:zk})end) end
) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{25.864065, "アイウエオかきくけこサシスセソたちつてと"}
約26秒です。
では、Rustのクレート版を計測してみましょう。
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_ -> NifExample.half2kana("アイウエオかきくけこサシスセソたちつてと")end) end) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{39.003642, "アイウエオかきくけこサシスセソたちつてと"}
約39秒です。
え!? ElixirよりRustのほうが大分遅い!
Elixir速いじゃない !
・・・・・
っと。ここまでテンプレですね。
そんなはずはないです ^^。
Rustの最適化
Rustコンパイルして遅いというのは、**「Rustあるある」**のようです。
前回mix.exsの設定をご紹介しました。設定を見てみると、RustのコンパイルモードがMix.envに依存しています。ここはRustのコンパイルモードを、強制的にreleaseモードにして再コンパイルしてみます。
# ~
defp rustler_crates() do
[example: [
path: "native/example",
# mode: (if Mix.env == :prod, do: :release, else: :debug),
mode: :release # 強制的 releaseモードにする
]]
end
$ iex -S mix
iex(1)> :timer.tc(fn -> Enum.reduce(0..1_000_000, 0, fn _,_ -> NifExample.half2kana("アイウエオかきくけこサシスセソたちつてと")end) end) |> case do {elapsed, res} -> {elapsed/1000000, res} end
{4.450794, "アイウエオかきくけこサシスセソたちつてと"}
約4.45秒となりました。
elixirの約5.8倍速いという結果が出ました。
終わりに
これでRustのクレートを自由にElixirから使えるようになりました。次回は「
Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す」になります。
明日は @zacky1972 さんの「ZEAM開発ログv0.1.4 Python/NumPyとElixir/Flow一本勝負!ElixirはAI/ML業界に革命をもたらすか!?」す!