Elixir + Rustlerでベクトル演算を高速化しよう〜Rustler初級者編 1 〜

# はじめに
　この記事の発端は，**ピュアElixir**と**Elixir+Rustler**の実行速度がどれくらい違うのか，ベクトル演算を行うコードで比較してみようという感じで書かれています． 
　
シリーズとしては**ElixirありきでRust(Rustler)を使う人**をメインに据えています．

今回は Rust, Rustler の仕様について触れていきます． 

# 開発環境
- OTP-21.0.9
- Elixir(1.7.3)
- Rustler(0.18.0)
- Rust(1.28.0)

# 事前調査と方針
　Rust で行列演算を行うライブラリはすでにいくつかあります．

- Rusty-machine (rulinalg): Machine Learning library for Rust
- ndarray : Arrays inspired by numpy.
- nalgebra : Low dimensional linear algebra.
- servo/euclid : Basic linear algebra used by Servo.
- cgmath : Linear algebra for game development.

参照：「[Rustで数値計算してみた話](https://qiita.com/termoshtt/items/b27bae959ca4eab9920e)」

ですが，今回は1から演算を行う関数を自作しました．
自前の関数はジェネリクス(C++でいうとテンプレート)には対応していません．

# 環境構築
本稿では扱いません．
「[Elixirから簡単にRustを呼び出せるRustler #1 準備編](https://qiita.com/twinbee/items/aabc11d0d667800fc0bb)」
こちらを参照してください．

`mix.exs`ファイルの`app_name`やモジュール名はリンク先のものに準拠します

## 注意点
本稿で扱う`Rustler`は`v.0.18.0`なので，`mix.exs`の`deps`の設定には気をつけましょう．
主要な型に違いがあります．[Elixir-NIF-Rustボイラープレート Ruster0.17.1の注意点](https://qiita.com/twinbee/items/e005939c1bab53e60f6e#_reference-911a39ef57f73e92e16f)

```elixir:mix.exs
defp deps do
[
  { :Rustler,   "~> 0.18.0"},
]
end
```

# 基本構文
`ruslter`のプロジェクトを作成すると`add`関数が作成されています．

```Rust:lib.rs
fn add<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult<Term<'a>> {
        let num1: i64 = try!(args[0].decode());
        let num2: i64 = try!(args[1].decode());

        Ok((atoms::ok(), num1 + num2).encode(env))
}
```
## 解説
見慣れない子`<'a>`がいますね．[ライフタイム](https://doc.Rust-jp.rs/the-Rust-programming-language-ja/1.6/book/lifetimes.html)といいます．
非同期NIF呼び出しの際には気を配る必要がありますが，今は無視して問題ありません．

`Env`はBEAM(Erlang VM)との交信に必要です．
`args`はElixirからNIF経由で送られてきたデータですね． インデックスでアクセスしているように配列になっています．
`NifResult<T>`は`Result<T, Error>`と同値で，処理内容とエラーをまとめたものです．

上記の`add`を見てもらうと`decode`ではじまり，`encode`で終わっています．
そのままでは使えないようです．

## decodeの型
`Rust`では型推論ができますが， `decode`は型指定しないといけません． 
ためしに`add`の型指定を消してみましょう．

```
error[E0277]: cannot add `()` to `()`
  --> src/lib.rs:47:31
   |
47 |         Ok((atoms::ok(), num1 + num2).encode(env))
   |                               ^ no implementation for `() + ()`
   |
   = help: the trait `std::ops::Add` is not implemented for `()`
```

おそらく空のタプルになっており，計算できなくなってしまいます．
今回は単純なものをつくるので型多相(Polymorphic)にはしていませんが，今後対応させていくつもりです．
（多分Rustではなくて，Elixirの方で）

## エラーハンドリング
`try!()`はエラーのチェックをして，早期リターンをさせることができます．
似た構文として，`？`があります．
使い分けは使いたい型がプリミティブ型（初期で定義されている型）かどうかです．

プリミティブ型の例(`try!`)

- i64
- f64
- Rustler::ListIterator

プリミティブでない型(`?`)

- Vec<T>
- tuple

# ベクトル演算(内積)
## 設定

関数を足す際にはexportの設定をしましょう．

```Rust:lib.rs
Rustler_export_nifs! {
  "Elixir.NifExample",
  [
    //("Elixir's func, number of arguments, Rust's func)
    ("add", 2, add),
    ("dot_product", 2, dot_product),    
  ],
  None
}
```

忘れないうちにElixirの方でも書きます．

```elixir:
defmodule NifExample do
  use Rustler, otp_app: :phx_Rust, crate: :example

  def add(_a, _b), do: exit(:nif_not_loaded)
  def def dot_product(_a, _b), do: exit(:nif_not_loaded)
end
```

## コード
それではベクトル演算の関数を見ていきましょう．

1. `decode()`で`Vec<i64>`に変換
2. `zip()`で各配列の同じインデックスを持つ要素をペアにして，1つのtupleにまとめます
3. `for`で内積計算
4. `encode()`して`NifResult`を返す

```Rust:lib.rs
fn dot_product<'a>(env: Env<'a>, args: &[Term<'a>])-> NifResult<Term<'a>> {
  // 1
  let x: Vec<f64> = args[0].decode()?;
  let y: Vec<f64> = args[1].decode()?;
    
  // 2      
  let tuple = x.iter().zip(y.iter());
  
  // 3
  let mut ans = 0.0;
  for (i, j) in tuple{
    ans = ans + (i*j);
  }
  
  // 4
  Ok((atoms::ok(), ans).encode(env))
}
```


## 使い方 
`iex -S mix`でコンパイル，　関数を呼び出しましょう．

```elixir
iex(1)> NifExample.dot_product [1.0, 2.0, 3.0], [4.0, 5.0, 6.0]
{:ok, 32.0}
```

# パフォーマンス測定

要素1千万or一億をもつリストの内積計算を行う速度を比較します．
## コード 
以下，パフォーマンス測定に使用したコードです．

```elixir:example.ex
defmodule NifExample do
  use Rustler, otp_app: :phx_Rust, crate: :example

  # func with nif
  def dot_product(_a, _b), do: exit(:nif_not_loaded)
  def add(_a, _b), do: exit(:nif_not_loaded)
  
  # func for list
  def new(e1, e2) when e1 > e2, do: []
  def new(e1, e2) do
    [e1] ++ new(e1+1, e2)
  end

  # elixir func
  def dot_product_ex(r1, _r2) when r1 == [], do: 0
    def dot_product_ex(r1, r2) do
      [h1|t1] = r1
      [h2|t2] = r2
      (h1*h2) + dot_product_ex(t1, t2)
  end

  # Benchmark
  # none = elixir
  #   1 = call nif, Rustler
  def dp_benchmark do
    m = new(1, 10_000_000.0)
    :timer.tc( fn ->
      dot_product_ex(m, m) 
    end)
    |>elem(0)
    |>Kernel./(1_000_000)
  end

  def dp_benchmark1 do
    m = new(1, 10_000_000.0)
    :timer.tc( fn ->
      dot_product(m, m)
    end)
    |>elem(0)
    |>Kernel./(1_000_000)
  end
end
```

## 解説
### `new` 
自前で`new`を定義しています．理由は，工夫しないとElixirに`range`（例　1..10_000_000）を渡せないのでリストに変換する必要があるからです．しかし，`Enum.to_list`がとっても遅いので，自前の関数を使っています．
詳しくは以下のリンクをどうぞ．
@zacky1972 
「[ZEAM開発ログ v.0.3.3 GPU駆動ベンチマークで時間を食っていた「ある処理」を最適化することで，驚きのパフォーマンス改善となった件](https://qiita.com/zacky1972/items/9cff0114691feeeadc09)」

リンク先の`to_range`が私の環境(Rust 1.28.0)だとコンパイルエラーになるので，まだ使えていません．

### `dot_product_ex`
Elixir側で書いた内積計算プログラムです．

### `dp_benchmark` `dp_benchmark1`
無印側がElixirでの内積計算，`1`がついた方がRustでの内積計算です．

## 実行結果

※`Enum.to_list`が遅いとかいっておきながら，リストの作成時間は実行時間に入っていません．

|要素数|dp_benchmark(Elixir)|dp_benchmark1(Rust)|倍率|
|:-:|:-:|:-:|:-:|
|一千万|1.094141|0.47988|2.28|
||1.169583|0.518724|2.25|
|一億|37.05859|9.495681|3.90|
||33.451313|11.876747|2.81|

振れ幅大きいですね．．．なんとかできないしょうか

# まとめと感想
まとめとしてはこれだけです．

- elixir上のベクトル演算はRustlerを使用することで高速化が見込めそう

## 感想

- C++erなら馴染みやすいと聞いたんですが，まだよくわかりません． （おそらく私のC++知識が古い？ため）
- 型推論がある割に静的型付けをしないと`decode`で詰んでしまうので，結局英語で書かれたAPIやらを見に行かないといけないあたり敷居が高い気がします．
- Rustに限った話ではないと思いますが，コンパイルエラーが読めるかどうかで生産性が大きく変わることを再認識しました．

# 参考
- @twinbee 「[Elixirから簡単にRustを呼び出せるRustler #1 準備編](https://qiita.com/twinbee/items/aabc11d0d667800fc0bb)」
- 「[Rustlerドキュメント](https://docs.rs/Rustler/0.18.0/Rustler/index.html)」
- 「[プログラミング言語Rust](https://doc.Rust-jp.rs/the-Rust-programming-language-ja/1.6/book/README.html)」
- 「[[Rust] zipでfor文 & 速度比較](http://www.mathgram.xyz/entry/Rust/tips/zip)」