rayon

Rust のデータ並列処理ライブラリ。.iter() を .par_iter() に変えるだけで並列化が完了する API 設計と、Rust の型システムによるデータ競合の静的防止を両立する。内部は work-stealing スケジューラで駆動される。

作者は Niko Matsakis (Rust 言語チーム、borrow checker の設計者)。Rust エコシステムにおける CPU バウンド並列処理のデファクトスタンダード。

アーキテクチャ: 2クレート構成

rayon (1.12.0)              rayon-core (1.13.0)
┌──────────────────┐        ┌──────────────────────┐
│ 並列イテレータ    │        │ スレッドプール管理     │
│ ParallelIterator │───────→│ work-stealing        │
│ par_sort 等      │        │ join / scope / spawn │
│ ユーザー向け API  │        │ crossbeam-deque 依存  │
└──────────────────┘        └──────────────────────┘

分離の理由: 複数バージョンの rayon が同一バイナリに共存しても、グローバルスケジューラは1つだけであることを保証するため。rayon-core は v1.0 で安定化済み。

基本: par_iter

use rayon::prelude::*;

// 逐次版
let sum: i32 = data.iter().map(|&x| x * x).sum();

// 並列版 — .iter() を .par_iter() に変えるだけ
let sum: i32 = data.par_iter().map(|&x| x * x).sum();

par_iter() は &[T], &Vec<T>, &HashMap<K,V> 等に対して使える。par_iter_mut() で可変参照、into_par_iter() で所有権の消費。

主要 API

join — 二分岐の並列実行

let (left_sum, right_sum) = rayon::join(
    || compute_left(&data[..mid]),
    || compute_right(&data[mid..]),
);

scope — 動的タスク生成

rayon::scope(|s| {
    for item in &work_items {
        s.spawn(move |_| process(item));
    }
});
// ← ここで全タスクの完了が保証される

spawn — fire-and-forget

rayon::spawn(|| {
    heavy_background_work();
});

グローバルスコープにタスクを投入。結果の取得にはチャネル等が必要。

ThreadPoolBuilder — カスタムプール

let pool = rayon::ThreadPoolBuilder::new()
    .num_threads(4)
    .thread_name(|i| format!("worker-{i}"))
    .stack_size(8 * 1024 * 1024)
    .build()
    .unwrap();

pool.install(|| {
    // この中の rayon 操作はこのプールを使う
    data.par_iter().for_each(|x| process(x));
});

ParallelIterator トレイト設計

2層のトレイト

トレイト要件使える操作
ParallelIteratordrive_unindexed(consumer)for_each, map, filter, reduce, flat_map, find_any 等
IndexedParallelIterator上記 + len(), drive(consumer), with_producer(cb)上記 + zip, enumerate, skip, take, rev, collect_into_vec, position 等

IndexedParallelIterator は正確な長さが既知のイテレータ。Vec, slice, Range 等。filter() を通すと長さが不定になるため ParallelIterator に降格する。

内部の Producer / Consumer パターン

ParallelIterator チェーン:
  source.par_iter().map(f).filter(g).sum()

内部変換:
  Producer (データソース)
    → split_at(mid) で再帰的に二分割
    → 閾値以下で逐次 Iterator にフォールバック

  Consumer (集約操作)
    → split_at(mid) で対応する受け手を分割
    → 部分結果を reduce_with() で統合

bridge_producer_consumer 関数が両者を接続し、rayon::join() で再帰的に並列実行する。

分割戦略の制御

(0..1_000_000)
    .into_par_iter()
    .with_min_len(1000)    // これ以下には分割しない
    .with_max_len(10000)   // これ以上の塊は作らない
    .for_each(|x| compute(x));

並列ソート・チャンク等

// 並列ソート
let mut v = vec![5, 3, 1, 4, 2];
v.par_sort();                        // 安定ソート
v.par_sort_unstable();               // 不安定ソート (高速)
v.par_sort_by_cached_key(|x| expensive_key(x));

// 並列チャンク
data.par_chunks(100).for_each(|chunk| process(chunk));
data.par_chunk_by(|a, b| a / 10 == b / 10);

// 並列ウィンドウ (v1.12.0〜)
data.par_array_windows::<[_; 3]>().for_each(|w| { /* 3要素ずつ */ });

// 並列文字列
"hello world".par_split_whitespace().for_each(|word| { /* ... */ });

reduce と fold

use rayon::prelude::*;

// reduce: 連想操作 + 単位元
let max = data.par_iter()
    .copied()
    .reduce(|| i32::MIN, |a, b| a.max(b));

// fold: スレッドローカルな部分結果 → reduce で統合
let freq: HashMap<&str, usize> = words.par_iter()
    .fold(HashMap::new, |mut map, word| {
        *map.entry(word.as_str()).or_insert(0) += 1;
        map
    })
    .reduce(HashMap::new, |mut a, b| {
        for (k, v) in b { *a.entry(k).or_insert(0) += v; }
        a
    });

fold はスレッドごとに独立したアキュムレータを作る。reduce は結合的 (associative) である必要がある。

par_bridge — 逐次→並列変換

use rayon::iter::ParallelBridge;

let results: Vec<_> = reader.lines()
    .map(|l| l.unwrap())
    .par_bridge()            // 逐次イテレータを並列化
    .map(|line| process(&line))
    .collect();

内部で Mutex を使い逐次 next() を同期呼び出しする。next() 自体がボトルネックになりうる。可能なら par_iter() を直接使うべき。

パフォーマンス特性

特性詳細
スレッド数デフォルト = 論理コア数。ThreadPoolBuilder で設定可能
最小有効規模個々の操作が数マイクロ秒以上かかる場合に効果あり
join のコストスタックのみ。ヒープ割り当てなし
scope のコストタスクごとにヒープ割り当て
キャッシュ局所性LIFO 実行で良好。ただし OS のスレッドマイグレーションで無効化されうる
CPU pinning 効果sched_setaffinity で最大 20% 高速化の報告あり
逐次フォールバック分割が閾値以下になると通常の Iterator に変換して逐次実行

期待どおり速くならないとき

使うべき場面 / 使うべきでない場面

場面適否理由
CPU バウンドな map/filter/reduce最適データ並列の本領
大量データのソート最適par_sort が分割統治で並列化
再帰的分割統治最適join がそのまま対応
I/O バウンドタスク不適ブロッキングがスレッドプールを占有 → 飢餓
少量データ (数百以下)不適オーバーヘッド > 利得
async ランタイム内不適async ワーカーをブロック

async との共存パターン

// tokio 内から rayon を使うときは spawn_blocking で隔離
let result = tokio::task::spawn_blocking(move || {
    data.par_iter()
        .map(|x| heavy_computation(x))
        .collect::<Vec<_>>()
}).await.unwrap();

競合・代替との比較

ライブラリ用途モデル
rayonCPU バウンド データ並列work-stealing スレッドプール
std::thread低レベルスレッド操作OS スレッド直接管理
crossbeam同期プリミティブスコープ付きスレッド、チャネル、deque
tokioI/O バウンド非同期処理async タスク + イベントループ

バージョン履歴の重要な変更

バージョン変更
0.7rayon-core 分離。グローバルスケジューラの一意性保証
1.0 (2018)安定版リリース
1.1FIFO spawn (spawn_fifo, scope_fifo)
1.3タプル (最大12要素) の IntoParallelIterator (MultiZip)
1.4新スケジューラ (ターゲットウェイクアップ)。性能大幅改善
1.6fold_chunks。broadcast() API
1.7take_any / skip_any。yield_now() / yield_local()
1.8use_current_thread()。available_parallelism() ベースのスレッド数決定
1.9by_exponential_blocks / by_uniform_blocks。walk_tree 系
1.10par_chunk_by / par_chunk_by_mut
1.12par_array_windows。最小 Rust 1.85

押さえどころ(カード化候補)

関連

Graph