LoginSignup
12
5

More than 5 years have passed since last update.

Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる

Last updated at Posted at 2018-05-18

(この記事は、「Elixir or Phoenix Advent Calendar 2017」の25日目です)

前日は @tuchiroさんの 「ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する」でした。
本日は「Elixirから簡単にRustを呼び出せるRustler #1 準備編」の続きです。

:black_square_button::black_large_square::black_square_button: お知らせ :black_square_button::black_large_square::black_square_button:
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します
私もETSとFlowを交えた発表で参加します!
ストリームとデータ処理に興味ある方は、是非ともご参加下さい!

image.png


Rustler

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

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


Rustのクレートとは?

『クレートは他の言語における「ライブラリ」や「パッケージ」と同じ意味です。このことからRustのパッケージマネジメントツールの名前を「Cargo」としています。』公式サイトより。

というわけで、ElixirのmixにあたるツールがCargomix.exsにあたるのが、Cargo.tomlになります。今回は、Rustの外部クレートであるunicode-jpを使用します。その手順を見ていきましょう。

Rustlerでの設定

以下の手順で行います。

Rust側

Cargo.tomlに使用する外部クレートを追加します。

今回使用するクレートのunicode-jpCargo.tomlの依存関係の場所に追加します。このへんはmix.exsと同じ感覚です。mix deps.getにあたる動作は不要で、コンパイル時に読み込んでくれます。

./native/example/Cargo.toml
[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」で一切つながりがないのが不思議ですね:upside_down:)

./native/examle/src/lib.rs
#[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側

エラー処理のコードを追加するだけです。

./lib/example.ex

 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モードにして再コンパイルしてみます。

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

12
5
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
5