シリーズとしてはElixirありきでrust( rustler )を使う人をメインに据えています.
|> 「Elixir + Rustlerでベクトル演算を高速化しよう〜Rustler初級者編 1 〜」
※この記事は「Elixir + Rustlerでベクトル演算を高速化しよう」の「よりみち」的な立ち位置です.
はじめに
Elixirでベクトル演算を扱うならlistもしくはtupleが必要です.
※ちなみに私の投稿記事では一貫してlistを使っています.
([前回の記事](Elixir + Rustlerでベクトル演算を高速化しよう〜Rustler初級者編 1 〜)では,rustの方でtupleにしましたが)
となると必然的にlist作成を高速化したくなります.Enum.to_listが遅いって話は聞き逃せません.
これを機にlistのnewとzeros関数を自作して速度を測定してみました.
開発環境
- OTP-21.0.9
- Elixir(1.7.3)
- rustler(0.18.0)
- rust(1.28.0)
コード
以下が使用したコードです.
※内積計算の部分は省いています
@index 100_000_000で定数指定しています.
defmodule NifExample do
use Rustler, otp_app: :phx_rust, crate: :example
@index 100_000_000
# func with nif
def new(_a), do: exit(:nif_not_loaded)
def zeros(_a), do: exit(:nif_not_loaded)
# func for list
def new_ex(e1, e2) when e1 > e2, do: []
def new_ex(e1, e2) do
[e1] ++ new_ex(e1+1, e2)
end
def zeros_ex(n) when n < 1, do: []
def zeros_ex(n) do
[0] ++ zeros_ex(n-1)
end
def new_list_benchmark do
:timer.tc( fn ->
1..@index
|> Enum.to_list
end)
|>elem(0)
|>Kernel./(1_000_000)
end
def new_list_benchmark1 do
:timer.tc( fn ->
new_ex(1, @index)
end)
|>elem(0)
|>Kernel./(1_000_000)
end
def new_list_benchmark2 do
:timer.tc( fn ->
new(@index)
end)
|>elem(0)
|>Kernel./(1_000_000)
end
def zeros_benchmark do
:timer.tc( fn ->
List.duplicate(0, @index)
end)
|>elem(0)
|>Kernel./(1_000_000)
end
def zeros_benchmark1 do
:timer.tc( fn ->
zeros_ex(@index)
end)
|>elem(0)
|>Kernel./(1_000_000)
end
def zeros_benchmark2 do
:timer.tc( fn ->
zeros(@index)
end)
|>elem(0)
|>Kernel./(1_000_000)
end
end
実行
new
指定した数までlistを作成する
| Elixir enum | Elixir reursive | rust | |
|---|---|---|---|
| 実行時間(一千万) | 1.482213 | 0.593355 | 0.240299 |
| 倍率 | (1.00) | 2.49 | 6.16 |
| 実行時間(一億) | 13.485997 | 6.70341 | 4.409689 |
| 倍率 | (1.00) | 2.01 | 3.05 |
Enum.to_listと比較すると,自前の再帰呼び出しでも速度が上回っていますね.
zeros
指定した数だけ要素0をもつlistを返す
| List.duplicate | Elixir recursive | rust | |
|---|---|---|---|
| 実行時間(一千万) | 0.201562 | 0.55816 | 0.354279 |
| 倍率 | 2.76 | (1.00) | 1.57 |
| 実行時間(一億) | 1.8861 | 9.729135 | 6.261378 |
| 倍率 | 5.15 | (1.00) | 1.55 |
Enumのzerosに相当するものが見つからなかったので,Erlangの:lists.duplicateの測定をしました.
rustより速いんですよね.ソースコードを見に行ったら, 私が自前のElixirで実装した方法とは違う方法で再帰呼び出ししてます.
組み込み型に関数があるんならラップするだけでいいので自前の関数必要なかったですね.
def zeros(num) do
List.duplicate(0, num)
end
ただlistに対して++/2を使用するとパフォーマンスが落ちることはわかりました.
ちゃんとheadとtailを使いましょう.
まとめ
- やっぱり
Enum.to_list遅い -
listのnewはrustが速い- Erlangのソースコードを眺めたら
:lists.duplicateが応用できそうな気がするので一旦保留
- Erlangのソースコードを眺めたら
-
zeros,onesなら実装は組み込み型関数の利用で容易.
今後の記事予定
メイン
- NIF非同期呼び出しの実装
- 並列処理の実装(CPU)
- 並列処理の実装(GPU)
- Elixirからrustに
range(例 1..10_000_000 )を渡せるようにする関数to_rangeの実装
よりみちの続き
- Erlangの関数を呼び出す vs Erlangと同じ挙動のコードをElixirを書いて呼び出す
- パフォーマンスの比較
- 移植性?の確認
- 可読性の比較