(この記事は、「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を交えた発表で参加します!
ストリームとデータ処理に興味ある方は、是非ともご参加下さい!

image.png

Rustler

Rustといえば、メモリ安全なネイティブコンパイラ言語です。厳格なメモリ安全チェックで有名なのですが、Rustlerではその厳格な部分がうまいことラップされており、今回ご紹介するような文字列変換ではRustの既存ライブラリを、Rustのコンパイルエラーの嵐に巻き込まれずに、そのスピードの恩恵を受けることができます。

RustlerはElixirからNIF経由で安全にRustのモジュールを呼び出すためのライブラリとmix拡張を含めたボイラープレートを作成するパッケージです。
今回は、新たに関数を追加してRustのライブラリであるクレートをElixirから呼び出してみます。

    • hex 公式ドキュメント

 

    Github Rustler

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業界に革命をもたらすか!?」す!

广告
将在 10 秒后关闭
bannerAds