この記事はWanoアドベントカレンダーの10日目の記事です。
今回も何番煎じかな?みたいな内容でお送りいたします。
なお、Rust v1.31.0でRust 2018がリリースされましたので、前回辺りからRust 2018でコードを書いています。
ただRust 2018と言ってもextern crate
がサンプルコードから消えた程度の違いしかありません。
TL;DR
-
rayonの
ParallelIterator
にstd::iter::Iterator
と同様のmap
やfilter
やfold
等があり、これらが自動で並列処理される。 -
iter
メソッドと同様にpar_iter
メソッドを呼ぶとParallelIterator
が取れる。 -
par_sort
ってメソッドでソートも並列化してくれる。意外と並列化のオーバーヘッドが少なくて速い。 - rayonの詳細はこのブログ記事を参照
rayonとは
他の言語とかであったりするループの各イテレーションを並列化できるライブラリと同じような事が出来るライブラリです。
ただしループ処理ではなくイテレーターを並列化します。
rayonでコードを書いてみましょう
map
たとえば配列の値に応じたフィボナッチ数のうち最大の物をナイーブに求める場合はこんな感じになります。
use rand::prelude::*;
use rayon::prelude::*;
use std::time::Instant;
fn fib(i: u64) -> u64 {
match i {
0 => 1,
1 => 1,
_ => fib(i-2) + fib(i-1),
}
}
fn main() {
let mut rng = thread_rng();
let xs: Vec<_> = (0..100000).map(|_| rng.gen_range(0, 24)).collect();
let ys = xs.clone();
let begin = Instant::now();
let max = xs.par_iter().map(|&x| fib(x)).max(); // `par_iter`を使って並列化
println!("Parallel: {:?}; elapsed: {:?}", max, begin.elapsed());
let begin = Instant::now();
let max = ys.iter().map(|&x| fib(x)).max(); // 普通のIterator
println!("Sync: {:?}; elapsed: {:?}", max, begin.elapsed());
}
use rayon::prelude::*
で必要なtraitとか型とかが全て使えるようになります。
あとは par_iter
でParallelIteratorを取得したら、おなじみのmapとかを呼ぶだけです。
上のコードを動かしてみたら結果は次の様になりました。
Parallel: Some(46368); elapsed: 525.928662ms
Sync: Some(46368); elapsed: 2.659804912s
rayonを用いた方が5倍くらい速いですが、これは実行した環境が5コアのVMだからでしょう。
rayonは論理CPUコア数と同じ数のスレッドを用いて処理しますので、このコードの様に1イテレーションの処理が重くて他の処理に依存していない場合はいい感じにスケールします。
collect
処理結果をcollect
メソッドでVec
にする事もできます。
use rayon::prelude::*;
fn main() {
let xs = vec![1, 2, 3, 4, 5];
let ys: Vec<_> = xs.par_iter().map(|&x| x * x).collect();
println!("xs: {:?}", xs);
println!("ys: {:?}", ys);
}
出力:
xs: [1, 2, 3, 4, 5]
ys: [1, 4, 9, 16, 25]
お手軽ですね。
par_sort
par_sort
メソッドを使うとソートが出来るのでやってみましょう。
use rand::prelude::*;
use rayon::prelude::*;
use std::time::Instant;
fn main() {
let mut xs: Vec<i32> = (0..1000000).map(|_| random()).collect();
let mut ys = xs.clone();
let i = random::<usize>() % 1000000;
let begin = Instant::now();
xs.par_sort();
println!("[Parallel] xs[{}]: {}; elapsed: {:?}", i, xs[i], begin.elapsed());
let begin = Instant::now();
ys.sort();
println!("[Sync] ys[{}]: {}; elapsed: {:?}", i, ys[i], begin.elapsed());
}
結果:
[Parallel] xs[376272]: -527567671; elapsed: 40.352851ms
[Sync] ys[376272]: -527567671; elapsed: 60.221255ms
CPUが5コアのVMで100万要素の配列に対して実行して、rayonを用いた方がいくらか速いという感じになりました。
ただ、配列の要素数がもっと少なかったり、CPUコア数がもっと少ない場合にはrayonを用いた方がオーバーヘッドの分で遅くなる場合もあり得ます(これは他の処理でも言えます)。
終わりに
様々な事情で2コアのVMとかを使っている場合はあまり恩恵が無いかもしれませんが、多コア環境で実行するコードを書くような時は必要に応じて使用を検討してみると良いかもしれません。
少なくとも自前で頑張るよりは圧倒的に良いと思います。
rayon自体がどう動いているのか気になる方は下記のブログ記事で解説されているのでそちらをご覧ください(余裕があったら後で要約を書き足したいと思います)。
http://smallcultfollowing.com/babysteps/blog/2015/12/18/rayon-data-parallelism-in-rust/