(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の18日目,「Webパフォーマンス Advent Calendar 2017」の10日目です)
昨日は @twinbee さんの「Elixirから簡単にRustを呼び出せるRustler #5 NIFからメッセージを返す」でしたね。
お礼:各種ランキングに83回のランクインを達成しました
4/27から、37日間に渡り、毎日お届けしている「季節外れのfukuoka.ex Elixir Advent Calendar」と「季節外れのfukuoka.ex(その2) Elixir Advent Calender」ですが、Qiitaトップページトレンドランキングに12回入賞、Elixirウィークリーランキングでは6週連続で1/2/3フィニッシュを飾り、各種ランキング通算で、トータル83回ものランクインを達成しています
みなさまの暖かい応援に励まされ、合計552件ものQiita「いいね」もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、ますます季節外れのfukuoka.ex Advent Calendar、頑張っていきます
さて本題〜はじめに
今まで,ElixirからGPUを駆動しようと四苦八苦し,なんとかElixirからRustler経由でGPUを駆動するところまで行きました!
最終的には下記記事にまとまっています。
ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化
- Elixir/Rustler/OpenCL(GPU) は pure Elixir よりも1.75〜1.95倍高速です。
- Elixir/Rustler/OpenCL(GPU) は Elixir/Rustler/CPU よりも1.5〜1.83倍高速です。
- Erlang VM の実行コストと,リスト構造から配列に変換するコストが結構かかっています。リスト構造を最初から配列にする最適化も含めたElixirソースコードからの静的コンパイル/最適化をかけてやると,6.18〜6.88倍高速になる潜在的可能性があります。
- C言語の実行時間とRust CPUの実行時間はほぼ等しいです。最適化をかけた場合には,Rustそのものによる実行時間のオーバーヘッドはないものと考えて良さそうです。
- Python から pure Elixir は1.39〜1.54倍,Python から benchmarks9(Elixir/Rustler, OpenCL(GPU), inlining) は2.7倍,Python から Rust OpenCL は9.54倍の速度向上です。Python から Elixir に置き換えることで,このくらいの速度向上を期待できそうです
今回は小休止の小ネタということで,データとプログラムの紹介を割愛した benchmarks6 と benchmarks7 で使った,Elixir/Rustlerで,リストからバイナリに変換し,バイナリを引数にしてロジスティック写像のベンチマークを駆動するテクニックについてご紹介します。
結論から言えば,パフォーマンスの改善にはつながらなかったですし,車輪の再発明になっちゃったのですが,ここで用いられているテクニック自体は他に応用しがいがあるものだと思いましたので,紹介したいと思います。
Elixir + Rustler でネイティブコードが使えるようになると,ウェブのパフォーマンスが向上すると思います!
Elixir でのリスト→バイナリ変換
まずはコード見てください。
list
|> Enum.reduce("", fn (x, acc) -> acc<><<x>> end)
-
Enum.reduce
では,x
とacc
を引数に取る関数を次々と呼び出してリストをマージしていきます。x
にはリストの各要素が入り,acc
には累積された計算結果が入ります。 - この関数の内部で, 前回までの
acc
で累積されたバイナリの末尾にx
を追加して次回のacc
として返します。 - この作用により,たとえば
[1, 2, 3, 4, 5]
を与えると,<<1>>
→<<1, 2>>
→<<1, 2, 3>>
→<<1, 2, 3, 4>>
→<<1, 2, 3, 4, 5>>
とバイナリとして累積していきます。
Rustler でのリスト→バイナリ変換
こんな感じです。
logistic_map_Nif.ex の一部
defmodule LogisticMapNif do
use Rustler, otp_app: :logistic_map, crate: :logistic_map
def to_binary(_list), 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};
use rustler::types::list::NifListIterator;
use rustler::types::binary::{ NifBinary };
rustler_export_nifs! {
"Elixir.LogisticMapNif",
[("to_binary", 1, to_binary)],
None
}
fn to_binary<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let iter: NifListIterator = try!(args[0].decode());
let res: Result<Vec<i64>, NifError> = iter
.map(|x| x.decode::<i64>())
.collect();
match res {
Ok(result) => Ok(result.iter().map(|i| unsafe {
let ip: *const i64 = i;
let bp: *const u8 = ip as *const _;
let _bs: &[u8] = {
slice::from_raw_parts(bp, mem::size_of::<i64>())
};
*bp
}).collect::<Vec<u8>>()
.iter().map(|&s| s as char).collect::<String>()
.encode(env)),
Err(err) => Err(err),
}
}
これ,めっちゃ難しいです!
まず,下記で引数に与えられたリストを受け取ります。
let iter: NifListIterator = try!(args[0].decode());
let res: Result<Vec<i64>, NifError> = iter
.map(|x| x.decode::<i64>())
.collect();
次に map
に展開します。
match res {
Ok(result) => Ok(result.iter().map(|i|
ここまでは良いとして,この後で, i64
から u8
に変換してやるので, unsafe
を宣言する必要があります。一般に型を無視したい時,C言語で言うところのキャストをしたい時には unsafe
が必要です。
match res {
Ok(result) => Ok(result.iter().map(|i| unsafe {
で,次の2行で作業に用いるポインタを宣言します。
let ip: *const i64 = i;
let bp: *const u8 = ip as *const _;
後半は, ip
と bp
を型なしの共通のポインタにしますよ,ということで,C言語のキャストに当たることをしています。
さらにダメ押しで,下記の記述で強制的に前述の2つを一致させます。
let _bs: &[u8] = {
slice::from_raw_parts(bp, mem::size_of::<i64>())
};
全体の値として,この後でポインタの値を使いたいので,次の記述を書いて締めます。
*bp
ここらへんまで,初見じゃわからないですね。私も StackOverFlow かなんかに書いてある Q&A から根性で探し当てました。
このあとは,下記の記述で Vec
に収集します。
}).collect::<Vec<u8>>()
その後, @twinbee さんの「Elixirから簡単にRustを呼び出せるRustler #4 SHIFT-JIS変換を行う」で紹介されている文字列変換を使ってバイナリにします。
.iter().map(|&s| s as char).collect::<String>()
最後に Elixir に送ってやります。
.encode(env)),
いや〜やばかった。3時間以上ここで頭をひねったんじゃないかと思います。めっちゃ頭を使いました! Rust のめっちゃ難しい領域を垣間見ました。
バイナリからのロジスティック写像のベンチマークの呼び出し
これは先ほどのに比べればはるかに簡単です。
lib/logistic_map_Nif.ex の一部
defmodule LogisticMapNif do
use Rustler, otp_app: :logistic_map, crate: :logistic_map
def map_calc_binary(_binary, _num, _p, _mu), do:
:erlang.nif_error(:nif_not_loaded)
end
native/logistic_map/src/lib.rs の一部
#[macro_use] extern crate rustler;
#[macro_use] extern crate lazy_static;
use rustler::{NifEnv, NifTerm, NifResult, NifEncoder, NifError};
use rustler::types::list::NifListIterator;
use rustler::types::binary::{ NifBinary };
rustler_export_nifs! {
"Elixir.LogisticMapNif",
[("map_calc_binary", 4, map_calc_binary)],
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_binary<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
let in_binary : NifBinary = 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 = in_binary.iter().map(|&s| s as i64).map(|x| loop_calc(num, x, p, mu)).collect::<Vec<i64>>();
Ok(res.encode(env))
}
下記がバイナリの受け取りです。
let in_binary : NifBinary = args[0].decode()?;
下記でバイナリを走査して, loop_calc
を呼び出します。
let res = in_binary.iter().map(|&s| s as i64).map(|x| loop_calc(num, x, p, mu)).collect::<Vec<i64>>();
この loop_calc
の部分を任意の関数に置き換えてあげれば,バイナリの各要素に対して,その関数を適用することができます。
と,ここまで書き進めたところで。。。
衝撃の事実! なんとリスト→バイナリ変換は下記でOKとのこと。。。
list
|> to_string
iex(1)> [1, 2, 3, 4, 5] |> to_string
<<1, 2, 3, 4, 5>>
チャンチャン。orz
まとめ
次のことが実現できました。
- Elixir と Rustler それぞれで,リストからバイナリに変換する方法を実装しました。Elixir だと衝撃の
to_string
一発でOKだったという。。。 - Rustler でバイナリを読み取って,ロジスティック写像などの関数を適用する方法を実装しました。
今回の Rust プログラミングはかなりヤバかったです! Rust プログラミングの極めて難しい部分を垣間見た感じです。おかげさまで,だいぶ鍛えられましたw
今までの成果を論文にするため,少しの間だけお休みします。次回連載を楽しみにしてください!
明日は @koga1020 さんの「PhoenixでMicrosoft Translator テキスト APIを利用してみる」です! お楽しみに!
p.s.「いいね」よろしくお願いします
よろしければ,ページ左上の や のクリックをお願いしますー
ここの数字が増えると,書き手としては「ウケている」という感覚が得られ,連載を更に進化させていくモチベーションになりますので,もっとElixirネタを見たいというあなた,私たちと一緒に盛り上げてください!