ZEAM開発ログv0.1.5 Elixir から Rustler でネイティブコードベンチマークを呼び出してみよう〜ElixirでAI/MLを高速化

（この記事は、[「fukuoka.ex(その2) Elixir Advent Calendar 2017」](https://adventar.org/calendars/2871)の6日目，[「機械学習と数学 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ml_and_math2017)の5日目です)

昨日は @twinbee さんの[「Elixirから簡単にRustを呼び出せるRustler#3 いろいろな型を呼び出す」](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)でしたね。

[「ZEAM開発ログ 目次」はこちら](https://qiita.com/zacky1972/items/70593ab2b70d192813df)

# おしらせ

:stars::stars::stars::stars::stars:<b>　満員御礼！Elixir MeetUpを6月末に開催します　</b>:stars::stars::stars::stars::stars:
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します
私も現在連載中のElixirのGPU駆動について発表します！

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)

# さて本題〜はじめに

こんにちは， @zacky1972 こと山崎進です。ElixirでAI/MLを高速化する研究に取り組んでいます。現状としては，ElixirからGPUを駆動しようとしていて，今までにOpenCLでGPUを駆動するサンプルプログラムを公開しました。

本連載の前回記事はこちら
|> [ZEAM開発ログv0.1.0 Flow / GenStage による並列プログラミング入門](https://qiita.com/zacky1972/items/e843607881bbeca34b70)
|> [ZEAM開発ログv0.1.1 AI/MLを爆速にしたい！ Flow / GenStage でGPUを駆動できないの？](https://qiita.com/zacky1972/items/140d2380dfdf727b22bc)
|> [ZEAM開発ログv0.1.2 AI/MLを爆速にしたい！ Flow のコードを OpenCL で書いてみる〜CPU編](https://qiita.com/zacky1972/items/c5c43794bd8af75a9800)
|> [ZEAM開発ログv0.1.3 AI/MLを爆速にしたい！ Flow のコードを OpenCL で書いてみる〜GPU編](https://qiita.com/zacky1972/items/05ab840561657da1e154)
|> [ZEAM開発ログv0.1.4 Python/NumPyとElixir/Flow一本勝負！ElixirはAI/ML業界に革命をもたらすか!?](https://qiita.com/zacky1972/items/5e7a31b3ee03bc0d31dd)

今までのまとめとしては次の通りです。

1. OpenCLでGPUを利用した時にはC言語の1並列と比べて3.95倍の速度向上，Elixirの同等プログラムと比べて10.8〜11.9倍の速度向上になりました。
2. OpenCLを使わずにマルチコアかつSIMD命令やAVX命令を使った場合は，GPUの場合より高速になる可能性があります。見積もりではElixirの同等プログラムと比べて13倍前後の速度向上を期待できそうです。
3. GPUの場合は，データの転送に時間がかかっているので，データの転送量に比べて演算負荷が大きくなればなるほど，CPUよりGPUの方が有利になると思われます。
4. CPUとGPUで適性を見極めて適切に負荷分散をすること，さらにCPUとGPUを並列実行することで，さらなるパフォーマンスを引き出せる可能性があります。
5. Elixir / Flow は Python / NumPy より1.5倍前後速いです。
6. OpenCL(GPU) は Python / NumPy より17倍前後速いです。
7. 今後，研究・開発が進んだ暁には，PythonからElixirに移行することで，AI/ML処理の実行速度が大幅に向上する可能性があります。

今回はElixirからネイティブコードで書かれたロジスティック写像のベンチマークを呼び出してみます。これは，ElixirからGPUを駆動するにあたってネイティブコードを実行する必要があるので，その準備にあたります。 @twinbee さんが Rustler を使って下記のような連載をしていますので，参考にしながら進めていきましょう。

@twinbee さんのRustler連載はこちら
|> [Elixirから簡単にRustを呼び出せるRustler #1 準備編](https://qiita.com/twinbee/items/aabc11d0d667800fc0bb)
|> [Elixirから簡単にRustを呼び出せるRustler #2 クレートを使ってみる](https://qiita.com/twinbee/items/54e8a4ec73bc27abd10e)

# Rustler による1回のロジスティック写像のコード

Elixir のロジスティック回帰のプログラムを Rustler で実行できるように改造しました。Elixir に加えて，Rust のインストールが済んでいる前提です。

[GitHub はこちら](https://github.com/zeam-vm/logistic_map)

lib/logistic_map_Nif.ex の一部

```elixir
defmodule LogisticMapNif do
    use Rustler, otp_app: :logistic_map, crate: :logistic_map

    # When your NIF is loaded, it will override this function.
    def calc(_x, _p, _mu), do: :erlang.nif_error(:nif_not_loaded)
end
```

native/logistic_map/src/lib.rs の一部

```rust
#[macro_use] extern crate rustler;
#[macro_use] extern crate lazy_static;

use rustler::{NifEnv, NifTerm, NifResult, NifEncoder, NifError};

mod atoms {
    rustler_atoms! {
        atom ok;
        //atom error;
        //atom __true__ = "true";
        //atom __false__ = "false";
    }
}

rustler_export_nifs! {
    "Elixir.LogisticMapNif",
    [("calc", 3, calc)],
    None
}

fn calc<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
    let x: i64 = try!(args[0].decode());
    let p: i64 = try!(args[1].decode());
    let mu: i64 = try!(args[2].decode());

    Ok((atoms::ok(), mu * x * (x + 1) % p).encode(env))
}
```

以上で，Elixirの `LogisticMapNif.calc` を呼び出すと，Rustの `calc`を呼び出すことができます。順にコードを見ていきます。

```elixir
defmodule LogisticMapNif do
    use Rustler, otp_app: :logistic_map, crate: :logistic_map

    # When your NIF is loaded, it will override this function.
    def calc(_x, _p, _mu), do: :erlang.nif_error(:nif_not_loaded)
end
```

`do`以下で NIF(Native Implemented Functions)を呼び出すという宣言をしています。引数に`_`をつけることで，変数を使用していないという警告を抑制しています。

```rust
rustler_export_nifs! {
    "Elixir.LogisticMapNif",
    [("calc", 3, calc)],
    None
}
```

この記述は Rustler の設定です。Elixir の `"LogisticMapNif"`モジュールの`"calc"`関数を呼び出した時に Rust の `calc` 関数を呼び出します。この例では関数が同じ名称になっているのでわかりにくいですが，二重引用符 "" がついている方が Elixir の関数名，ついていないほうが Rust の関数名です。数字の`3`は引数の数を表します。

```rust
fn calc<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
    let x: i64 = try!(args[0].decode());
    let p: i64 = try!(args[1].decode());
    let mu: i64 = try!(args[2].decode());

    Ok((atoms::ok(), mu * x * (x + 1) % p).encode(env))
}
```

Rust の `calc` 関数の本体です。最初の`fn calc<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>>`は，Rustlerを使った場合，どんな関数であってもまったく同じ型をしています。実行時環境 `NifEnv` と項 `NifTerm` を受け取って項を要素に持つ結果 `NifResult<NifTerm>`を返します。

`let x: i64 = try!(args[0].decode()); let p: i64 = try!(args[1].decode()); let mu: i64 = try!(args[2].decode());` では項から引数を読み取っています。`i64`は64ビットの整数型で，Elixirの整数型に対応します。ここで型が合わないとエラーになります。


`Ok((atoms::ok(), mu * x * (x + 1) % p).encode(env))` では計算して結果を返しています。`atoms:ok()` はElixirで書くと`:ok`というアトムに対応します。`mu * x * (x + 1) % p` はロジスティック写像の計算部分です。整数演算はC言語とほぼ同じです。`(atoms::ok(), mu * x * (x + 1) % p)`と括ることで Elixir のタプルに相当する構造体を構成し，`encode(env)`でElixirの表現形式に変換します。`OK(...)`によって正常終了であることを示します。 @twinbee さんの[「Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す」](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)も参照するといいですよ。

以上の実行結果は次のような感じです。

```
iex(1)> LogisticMapNif.calc(1,61,22)
{:ok, 44}
```

# Rustler によるロジスティック写像ベンチマーク

ロジスティック写像の1回の呼び出しだけをネイティブコードで呼び出しても速度向上は少ないと思われますので，まとまった演算をネイティブコード化してみます。

lib/logistic_map_Nif.ex の一部

```elixir
defmodule LogisticMapNif do
    use Rustler, otp_app: :logistic_map, crate: :logistic_map

    def map_calc_list(_list, _num, _p, _mu), do: :erlang.nif_error(:nif_not_loaded)
end
```

インタフェースとしては，第1引数としてリストを受け取り，1つ1つの要素を `x` として `num`回のロジスティック写像の計算を行います。

Elixirのコードで書くと次のような計算をRustで書いてみましょう。

```elixir
  def loopCalc(num, x, p, mu) do
    if num <= 0 do
      x
    else
      loopCalc(num - 1, calc(x, p, mu), p, mu)
    end
  end

  def mapCalc(list, num, p, mu, stages) do
    list
    |> Enum.map(& loopCalc(num, &1, p, mu))
    |> Enum.to_list
  end
```

```rust
#[macro_use] extern crate rustler;
#[macro_use] extern crate lazy_static;

use rustler::{NifEnv, NifTerm, NifResult, NifEncoder, NifError};
use rustler::types::list::NifListIterator;

rustler_export_nifs! {
    "Elixir.LogisticMapNif",
    [("map_calc_list", 4, map_calc_list)],
    None
}

fn loop_calc(num: i64, init: i64, p: i64, mu: i64) -> i64 {
    let mut x: i64 = init;
    for _i in 0..num {
        x = mu * x * (x + 1) % p;
    }
    x
}

fn map_calc_list<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
    let iter: NifListIterator = try!(args[0].decode());
    let num: i64 = try!(args[1].decode());
    let p: i64 = try!(args[2].decode());
    let mu: i64 = try!(args[3].decode());

    let res: Result<Vec<i64>, NifError> = iter
        .map(|x| x.decode::<i64>())
        .collect();

    match res {
        Ok(result) => Ok(result.iter().map(|&x| loop_calc(num, x, p, mu)).collect::<Vec<i64>>().encode(env)),
        Err(err) => Err(err),
    }
}
```

まず

```rust
rustler_export_nifs! {
    "Elixir.LogisticMapNif",
    [("map_calc_list", 4, map_calc_list)],
    None
}
```

ここでRustlerの設定を記述して Elixir のコードと Rust のコードの対応関係を記述しています。何が書いてあるかは大体もうわかりますね？ 数字の`4`は引数の数ですよ。

```rust
fn loop_calc(num: i64, init: i64, p: i64, mu: i64) -> i64 {
    let mut x: i64 = init;
    for _i in 0..num {
        x = mu * x * (x + 1) % p;
    }
    x
}
```

これはElixirから直接呼ばれないRustの関数`loop_calc`を定義しています。

* `fn loop_calc(num: i64, init: i64, p: i64, mu: i64) -> i64` は，64ビット整数型の `num, init, p, mu` を引数にして64ビットの整数型を返す関数 `loop_calc`の宣言です。
* `let mut x: i64 = init;` ですが，変数宣言に`mut`と書くことで，ミュータブル，つまり再代入可能だと宣言しています。Rustの変数はデフォルトでイミュータブル，つまり1回値が決まると変更できないという設定になっています。
* `for _i in 0..num` はC言語で書くと `for(int i = 0; i < num; i++)`です。`i`の値を後で使わないので，`_i`として警告を抑制しています。
* `x = mu * x * (x + 1) % p;` はロジスティック写像の漸化式にあたります。`x`に再代入していますね。
* 最後の`x`は，xの値を返すという意味になります。Rustでは，Rubyのように，最後に評価された式が関数全体の戻り値になります。

```rust
fn map_calc_list<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
    let iter: NifListIterator = try!(args[0].decode());
    let num: i64 = try!(args[1].decode());
    let p: i64 = try!(args[2].decode());
    let mu: i64 = try!(args[3].decode());

    let res: Result<Vec<i64>, NifError> = iter
        .map(|x| x.decode::<i64>())
        .collect();

    match res {
        Ok(result) => Ok(result.iter().map(|&x| loop_calc(num, x, p, mu)).collect::<Vec<i64>>().encode(env)),
        Err(err) => Err(err),
    }
}
```

* Elixirから呼ばれるRust関数は`fn map_calc_list<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>>`という型をしています。
* `let iter: NifListIterator = try!(args[0].decode());`では第1引数のリストを読み込んでいます。Elixirのリストに対応する構造体は`NifListIterator`です。この時点ではまだ先頭要素だけを読み込んでいます。
* `let res: Result<Vec<i64>, NifError> = iter.map(|x| x.decode::<i64>()).collect();`では，リストの各要素を読み込んで整数型のベクター`Vec<i64>`化しています。型が合わないとエラーになることがあるので，直接`Vec<i64>`の型になるのではなく，`Result<Vec<i64>, NifError>`型で受けます。
* リストの読み込み方については @twinbee さんの[「Elixirから簡単にRustを呼び出せるRustler #3 いろいろな型を呼び出す」](https://qiita.com/twinbee/items/f94eb7f74ff39c781da0)も参照するといいですよ。
* `iter.map(...).collect()`という形式は，MapReduce的な計算，Elixirでいうと，`list |> Enum.map(...) |> Enum.to_list` に相当します。
* `iter.map(|x| x.decode::<i64>())`で，Elixir形式の各要素を整数型として読み込みます。これによりリストをまず1回走査することになります。
* `match res` 以下で，リストの各要素が全て整数型だった時には`Ok(result) =>`以下を実行し，どれか整数型ではなかった場合には `Err(err) =>` 以下を実行します。ちょうど `try ... catch ...` 構文に相当しますね。
* `result.iter().map(|&x| loop_calc(num, x, p, mu)).collect::<Vec<i64>>().encode(env)` はおなじみ MapReduce 的な構文で `loop_calc` 関数を呼び出しています。`collect::<Vec<i64>>()`をもし単に`collect()`とすると，型推論に失敗してエラーになります。ここでは`collect`で集めた結果を `Vec<i64>`型であると注釈をつけています。これは慣れないと難しいですね。この辺りが Rust プログラミングの難しさの一端になっています。ここでも1回走査します。
* 全体として2回走査します。これをうまく1回の走査で実現できれば高速化の余地がありますが，型エラーに阻まれてうまくプログラミングできませんでした。

ここまでで実行してみると，ちゃんと動作します。

```
iex(1)> [1,2,3] |> LogisticMapNif.map_calc_list(10, 61, 22)
[28, 25, 37]
```

ではベンチマークプログラムに組み込んでみましょう。次のように実行してみたとします。

lib/logistic_map.ex

```elixir
defmodule LogisticMap do
  @logistic_map_size      0x2000000
  @default_prime 6_700_417
  @default_mu 22
  @default_loop 10

  def mapCalc5(list, num, p, mu, stages) do 
    list
    |> Enum.to_list
    |> LogisticMapNif.map_calc_list(num, p, mu)
  end

  @doc """
  Benchmark
  """
  def benchmark5(stages) do
    IO.puts "stages: #{stages}"
    IO.puts (
      :timer.tc(fn -> mapCalc5(1..@logistic_map_size, @default_loop, @default_prime, @default_mu, stages) end)
      |> elem(0)
      |> Kernel./(1000000)
    )
  end

  @doc """
  Benchmarks
  """
  def benchmarks5() do
    [1, 2, 4, 8, 16, 32, 64, 128]
    |> Enum.map(& benchmark5(&1))
    |> Enum.reduce(0, fn _lst, acc -> acc end)
  end
end
```

`mapCalc5` で `Enum.to_list` を挟んでいるのは，Rustler のコードが範囲オブジェクトに対応していないためです。

では，実行してみます。

```
iex(1)> LogisticMap.benchmark5(1)
stages: 1
16.628159
```

現時点では並列実行していませんから，なかなかの速さではないでしょうか。

では気を良くして Flow を使って並列実行してみましょう。

mapCalc5 のみ変更

```elixir
  def mapCalc5(list, num, p, mu, stages) do 
    list
    |> Stream.chunk_every(stages + 1)
    |> Flow.from_enumerable(stages: stages)
    |> Flow.map(fn(i) -> 
    	i 
    	|> LogisticMapNif.map_calc_list(num, p, mu)
    	end)
    |> Enum.to_list
    |> List.flatten
  end
```

やりたいことは，Flow の並列実行機能は使いつつ，リストの要素自体はひとまとめにして渡したいということです。そこで，`Stream.chunk_every`を使って並列実行したい数にリストを分割してから Flow に渡してあげます。最後に `List.flatten` で平滑化します。

実行してみましょう。

あれ？ かえって遅くなったような。。。

実は，`Stream.chunk_every` を使って Flowを起動するのにオーバーヘッドがかかるのと，NIFを呼び出して完了するまでの時間を1ms以内に抑えないとVMが停止してしまいパフォーマンスが落ちてしまっているという2つの原因で実行速度が落ちているのです。

後者の対策としては，一度に渡すリストの長さを短くすることが有効です。そこで，次のようにしてみました。

```elixir
  @logistic_map_chunk_num 0x400

  def mapCalc5(list, num, p, mu, stages) when stages <= 1 do
    list
    |> Enum.to_list
    |> LogisticMapNif.map_calc_list(num, p, mu)
  end
  def mapCalc5(list, num, p, mu, stages) when stages > 1 do
    chunk_size = div(Enum.count(list) - 1, stages) + 1
    list
    |> Stream.chunk_every(chunk_size)
    |> Flow.from_enumerable(stages: stages)
    |> Flow.map(fn(i) ->
    	i
    	|> Stream.chunk_every(@logistic_map_chunk_num)
    	|> Enum.map(fn(j) ->
    		j
    		|> LogisticMapNif.map_calc_list(num, p, mu)
    		end)
    	end)
    |> Enum.to_list
    |> List.flatten
  end
```

高速化のため，`stages = 1` のときには Flow を使わずに直接駆動しています。`1`より大きい時には，リスト長を取得して `chunk_size` を計算し， `Flow.map`の中で，`Stream.chunk_every`と`Enum.map`をつなげてリストを分割しています。1回のNIF呼び出しで渡すリストのチャンクサイズは `@logistic_map_chunk_num` で与えます。

ほかにRustでつくった NIF 関数は次のものがあります。興味があったら，ソースコードを読んでみてください。相当苦労しましたが，速度面でそれほど報われませんでしたw

[GitHub はこちら](https://github.com/zeam-vm/logistic_map)

* `to_binary`: リストからバイナリに変換する NIF 関数。とっても苦労しました！ そのうち解説を書きます。
* `map_calc_binary`: バイナリ版の `map_calc`

# Flow.Window による Rustler 並列処理の高速化

`mapCalc5`もなかなか高速なのですが，並列化した時にもっと高速化できるのではないかと期待してしまいます。

@twinbee さんに相談したところ， `Flow.Window` という便利な機能があると教わったので， @twinbee さんと一緒に探求してみました！

`Flow.Window` は並列処理するにあたって処理を分配した後，一定の長さや時間の処理をするところで区切りを設けて，次の処理の分配を行うという機能です。

`Flow.Window` を使ったコードはこちらです。なお `stage = 1` のときは `mapCalc5` と同じ動作です。

```elixir
  @logistic_map_chunk_num 0x400

  def mapCalc8(list, num, p, mu, stages) when stages <= 1 do
    list
    |> Enum.to_list
    |> LogisticMapNif.map_calc_list(num, p, mu)
  end
  def mapCalc8(list, num, p, mu, stages) when stages > 1 do
    window = Flow.Window.global
    |> Flow.Window.trigger_every(@logistic_map_chunk_num, :reset)

    list
    |> Flow.from_enumerable
    |> Flow.partition(window: window, stages: stages)
    |> Flow.reduce(fn -> [] end, fn e, acc -> [e | acc] end)
    |> Flow.map_state(& &1 |> LogisticMapNif.map_calc_list(num, p, mu))
    |> Flow.emit(:state)
    |> Enum.to_list
  end
```

[GitHub はこちら](https://github.com/zeam-vm/logistic_map)

# 1ms問題を解決するためのキャリブレーション機能

NIFの呼び出しは実行時間が1ms以内でないとVMが停止してパフォーマンスが極端に落ちるという問題があるので，1ms以内に収まるようにリスト長を調整するキャリブレーション機能を実装してみました。

lib/logistic_map_Nif.ex

```elixir
defmodule LogisticMapNif do
  use Rustler, otp_app: :logistic_map, crate: :logistic_map

  # When your NIF is loaded, it will override this function.
  def calc(_x, _p, _mu), do: :erlang.nif_error(:nif_not_loaded)

  def map_calc_list(_list, _num, _p, _mu), do: :erlang.nif_error(:nif_not_loaded)

  def to_binary(_list), do: 
  :erlang.nif_error(:nif_not_loaded)

  def map_calc_binary(_binary, _num, _p, _mu), do:
  :erlang.nif_error(:nif_not_loaded)

  def floor(value, precision \\ 1) do
    Float.floor(value / precision) * precision |> Kernel.trunc
  end

  def get_env(key) do
  	System.get_env(key) |> String.to_integer
  end

  def put_env(key, value) do
  	System.put_env(key, "#{value}")
  	"#{key}: #{value}\n"
  end

  def env_floor(key) do
  	  System.put_env(key, "#{Kernel.max(1, floor(get_env(key), 100))}")
  end

  def calibration(key, function, is_map_calc, length \\ 10) do
    input = 1..length |> Enum.to_list
	  {micro, _} = :timer.tc(fn -> function.(input) end)
  	ms = micro |> Kernel./(1000)
  	if ms >= 1 do
  		env_floor key
  		get_env key
  	else
  	  put_env(key, length)
  	  calibration(key, function, is_map_calc, length + 10)
  	end
	end

	def min_calibration(keywords, number \\ 10) do
	  key = keywords[:key]
	  function = keywords[:function]
	  is_map_calc = keywords[:is_map_calc]
		value = 1..number
		|> Enum.map(fn _ -> calibration(key, function, is_map_calc) end)
		|> Enum.min
		put_env(key, value)
	end

  def init do
  	[[key: "LogisticMapNif_map_calc_list", function: fn x -> map_calc_list(x, 10, 61, 22) end, is_map_calc: true],
  	 [key: "LogisticMapNif_map_calc_binary", function: fn x -> x |> Enum.reduce("", fn (x, acc) -> acc<><<x>> end) |> map_calc_binary(10, 61, 22) end, is_map_calc: true],
  	 [key: "LogisticMapNif_map_calc_binary_to_binary", function: fn x -> x |> to_binary |> map_calc_binary(10, 61, 22) end, is_map_calc: true]]
  	|> Enum.map(& min_calibration(&1))
  	|> IO.puts
  end
end
```

[GitHub はこちら](https://github.com/zeam-vm/logistic_map)



# 実行結果

下記の環境での実行結果です。

> Mac Pro (Mid 2010)
> Processor 2.8GHz Quad-Core Intel Xeon
> Memory 16GB

実行結果を表にまとめてみました。またC言語やOpenCLでの実行結果を付記しています。

|stages|benchmark1 |benchmarks3                |benchmarks5          |benchmarks8|C|OpenCL1|OpenCL2|
|-----:|----------:|--------------------------:|--------------------:|-----:|----:|---:|--:|
|      |pure Elixir|pure Elixir                |Rustler              |Rustler|C|OpenCL|OpenCL|
|      |loop       |inlining inside of Flow.map|loop, passing by list|passing by list, with Window|loop|CPU, loop|CPU, inlining|
|     1|54.162408|42.181708|12.165364|10.138827|4.232451|-|-|
|     2|27.194265|24.057105|35.714096|17.302215|-|-|-|
|     4|17.949165|15.936957|35.665139|16.241035|-|-|-|
|     8|14.621331|13.465502|36.476310|16.663471|-|1.496656|1.483530|
|    16|14.823739|13.914704|38.289668|18.059605|-|-|-|
|    32|15.294675|13.257334|41.796529|21.647251|-|-|-|
|    64|15.303911|13.338678|40.862091|28.272938|-|-|-|
|   128|15.729719|13.180197|42.129716|40.542968|-|-|-|

* pure Elixir では論理コア数と等しい`stages=8`のときが最速ですが，Rustlerでは`stages=1`の時が最速です。並列プログラミングがうまく機能していませんね。おそらく`Flow`のオーバーヘッドが原因と思われます。
* `benchmarks8`ではだいぶ並列処理性能が改善されています。`Flow.Window`は比較的うまく機能するようです。
* 最速同士で比べると，pure Elixir → Rustlerで速度差はほとんどありませんでした。残念。
* `stages=1`のときの Rustler とC言語を比べるとまだまだ速度差は歴然としています。これはElixirの表現形式と相互変換する部分，とくにリストとベクターの相互変換に時間がかかっているものと思われます。C言語並みに高速化するには，リストをベクター化(配列化)する部分に工夫が要りそうです。LLVMで直にアセンブリコードを出力したらどうなるか興味があります！

# おわりに

1. Rustler を使っても pure Elixir から高速化できませんでした。
2. Flow は便利ですが，オーバーヘッドがかなりあります。
3. Rustler とC言語やOpenCLと比べるとまだまだ速度差は歴然としています。さらなる高速化に向けてはデータ表現の相互変換をどのように工夫するかが鍵になりそうです。LLVMで直にアセンブリコードを出力したらどうなるか興味があります！

そういうわけで今回は失意の結果ではあるのですが，次回は気を取り直してRustlerからGPUを駆動してみたいと思います。お楽しみに！

明日は @koga1020 さんの[「Elixirのパーサーコンビネータライブラリ Combine入門」](https://qiita.com/koga1020/items/e102dc56315800704602)です。こちらもお楽しみに！

# p.s.「いいね」よろしくお願いします

よろしければ，ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックをお願いしますー:bow:
ここの数字が増えると，書き手としては「ウケている」という感覚が得られ，連載を更に進化させていくモチベーションになりますので，もっとElixirネタを見たいというあなた，私たちと一緒に盛り上げてください！:tada: