Rust：rayonでお手軽・安全に並列処理

この記事は[Wanoアドベントカレンダー](https://qiita.com/advent-calendar/2018/wano-group)の10日目の記事です。

今回も何番煎じかな？みたいな内容でお送りいたします。

なお、Rust v1.31.0でRust 2018がリリースされましたので、前回辺りからRust 2018でコードを書いています。
ただRust 2018と言っても`extern crate`がサンプルコードから消えた程度の違いしかありません。

## TL;DR

* [rayon](https://crates.io/crates/rayon)の`ParallelIterator`に`std::iter::Iterator`と同様の`map`や`filter`や`fold`等があり、これらが自動で並列処理される。
* `iter`メソッドと同様に`par_iter`メソッドを呼ぶと`ParallelIterator`が取れる。
* `par_sort`ってメソッドでソートも並列化してくれる。意外と並列化のオーバーヘッドが少なくて速い。
* rayonの詳細は[このブログ記事](http://smallcultfollowing.com/babysteps/blog/2015/12/18/rayon-data-parallelism-in-rust/)を参照

## rayonとは

他の言語とかであったりするループの各イテレーションを並列化できるライブラリと同じような事が出来るライブラリです。
ただしループ処理ではなくイテレーターを並列化します。

## rayonでコードを書いてみましょう

### map

たとえば配列の値に応じたフィボナッチ数のうち最大の物をナイーブに求める場合はこんな感じになります。

~~~~rust
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とかを呼ぶだけです。

上のコードを動かしてみたら結果は次の様になりました。

~~~~text
Parallel: Some(46368); elapsed: 525.928662ms
Sync: Some(46368); elapsed: 2.659804912s
~~~~

rayonを用いた方が5倍くらい速いですが、これは実行した環境が5コアのVMだからでしょう。
rayonは論理CPUコア数と同じ数のスレッドを用いて処理しますので、このコードの様に1イテレーションの処理が重くて他の処理に依存していない場合はいい感じにスケールします。

### collect

処理結果を`collect`メソッドで`Vec`にする事もできます。

~~~~rust
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);
}
~~~~

出力：

~~~~text
xs: [1, 2, 3, 4, 5]
ys: [1, 4, 9, 16, 25]
~~~~

お手軽ですね。

### par_sort

`par_sort`メソッドを使うとソートが出来るのでやってみましょう。

~~~~rust
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());
}
~~~~

結果：

~~~~text
[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/