LoginSignup
13
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-09

この記事はWanoアドベントカレンダーの10日目の記事です。

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

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

TL;DR

  • rayonParallelIteratorstd::iter::Iteratorと同様のmapfilterfold等があり、これらが自動で並列処理される。
  • 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/

13
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10