PythonとRustで並列処理を実装してみた ～パフォーマンス比較とその考察～

## 1. 目的

本記事では、Python（3.11）とRustという二つの異なるプログラミング言語における並列処理の実装方法について学びます。

また、RustのコードをPythonにモジュール化をしてパフォーマンスを比較し、なぜRustが優れているのかを探ります。

## 2. Pythonの並列処理

Pythonでは、標準ライブラリとして[`concurrent.futures`](https://docs.python.org/ja/3/library/concurrent.futures.html)モジュールが提供されており、その中でも`ThreadPoolExecutor`はスレッドベースの並列処理を簡単に実装できる便利なツールです。以下は、`ThreadPoolExecutor`を用いた簡単な並列処理の[例](https://github.com/makinzm/rust-parallel-with-python/blob/main/parallel.py)で,0からn-1までの数値の二乗を計算する関数を並列に実行し配列に格納しています。

```python
import concurrent.futures

def parallel_square_computation(n):
    def compute_square(i):
        return i * i

    with concurrent.futures.ThreadPoolExecutor() as executor:
        results = list(executor.map(compute_square, range(n)))
    
    return results
```

## 3. Rustの並列処理

Rustはシステムプログラミング言語として、高いパフォーマンスと安全性を両立しています。並列処理においても強力なサポートがあり、特に[`rayon`](https://docs.rs/rayon/latest/rayon/)クレートを利用することで簡潔かつ効率的に並列処理を実装できます。

### 3.1. Rayonの利用とその必要性

`rayon`はデータ並列処理を簡単に実現できるクレートであり、スレッドプールの管理やタスクの分割を自動的に行ってくれます。以下は、`rayon`を用いた並列処理の[例](https://github.com/makinzm/rust-parallel-with-python/blob/main/src/lib.rs)です。

```rust
use rayon::prelude::*;

fn parallel_computation(n: usize) -> Vec<usize> {
    (0..n).into_par_iter()
        .map(|i| i * i)
        .collect()
}
```

### 3.2. スレッドを用いた並列処理の難しさ

Rustでは標準ライブラリの[`std::thread`](https://doc.rust-lang.org/std/thread/index.html)を用いて並列処理を行うことも可能ですが、スレッドの生成や共有データの管理は手動で行う必要があり、コードが複雑化しがちです。例えば、[以下のような実装](https://github.com/makinzm/rust-parallel-with-python/blob/8f1dc9ba88a828ab071322cf7510439c6cc1f726/src/lib.rs)はosレベルのスレッドの生成数が多くなり悪化します。

```rust
use std::thread;
use std::sync::{Arc, Mutex};

fn parallel_computation(n: usize) -> Vec<usize> {
    let results = Arc::new(Mutex::new(vec![0; n]));
    let mut handles = vec![];

    for i in 0..n {
        let results = Arc::clone(&results);
        let handle = thread::spawn(move || {
            let mut data = results.lock().unwrap();
            data[i] = i * i;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    Arc::try_unwrap(results).unwrap().into_inner().unwrap()
}

fn main() {
    let n = 1000;
    let squares = parallel_computation(n);
    println!("{:?}", squares);
}
```

このように、スレッド数が増加するとオーバーヘッドが大きくなり、パフォーマンスが低下する可能性があります。（この内容はRustに限らず、並列処理全般に言えることです）。
- 参考: 
    - [第７回 オーバーヘッドと並列性能 - Embedded Multicore Consortium](https://www.embeddedmulticore.org/2024/06/20/%E7%AC%AC%EF%BC%97%E5%9B%9E-%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%98%E3%83%83%E3%83%89%E3%81%A8%E4%B8%A6%E5%88%97%E6%80%A7%E8%83%BD/)
    - [Reading 23: Locks and Synchronization](https://web.mit.edu/6.005/www/fa15/classes/23-locks/)

### 3.3. Rayonを用いた効率的な並列処理

`rayon::iter`などを利用することで、タスクを効率的に分散し上記の問題を解決してくれます。

- [IntoParallelIterator in rayon::iter - Rust](https://docs.rs/rayon/latest/rayon/iter/trait.IntoParallelIterator.html#tymethod.into_par_iter)
- [ParallelIterator in rayon::iter - Rust](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html)
- [rayon::iter - Rust](https://docs.rs/rayon/latest/rayon/iter/index.html)

## 4. 比較

Rustのコードをmaturinを用いてPythonのモジュールとしてビルドし、Pythonから呼び出すことで、両者のパフォーマンスを比較しました（[詳細なコード](https://github.com/makinzm/rust-parallel-with-python/tree/main)）。

実験内容としては、数字`n`を与えた際に`0`から`n-1`のそれぞれについて二乗を計算し配列に入れるということを100回行いました。

maturinの利用については本ドキュメントでは扱いませんが非常に簡単にできるため[maturinの公式ドキュメント](https://www.maturin.rs)や[上記の詳細コードのREADME.md](https://github.com/makinzm/rust-parallel-with-python/tree/main)などを参考にしてください。

下の画像はそれぞれの`n`の`100`回の試行に対する時間の分布です。
Rustの`rayon`を用いた並列処理はPythonの`ThreadPoolExecutor`よりも高速であることが確認できます。

![execution_times_comparison.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417525/8e6687d3-bde4-a5cb-6d9d-31e055d0ae51.png)


## 5. 考察

なぜ、同じ並列処理でここまでパフォーマンスに差が出るのか考察します。

### 5.1. PythonのGILの影響

PythonのGIL（Global Interpreter Lock）は、「一度に Python の バイトコード を実行するスレッドは一つだけであることを保証する仕組み(用語集より抜粋)」です。これにより、スレッドセーフな操作が保証されますが、一方でマルチスレッドによるCPUバウンドな処理の並列化を制限してしまいます。`ThreadPoolExecutor`を用いても、実際にはGILの影響でスレッドが同時に動作することができず、シングルスレッドと同程度のパフォーマンスとなることが多いです。

- GILについての公式ドキュメントはこちら: [用語集 — Python 3.13.0 ドキュメント](https://docs.python.org/ja/3/glossary.html#term-global-interpreter-lock)
- I/Oのタスクの場合はGILの影響を減らせることがこちらに記述されています: [concurrent.futures --- 並列タスク実行 — Python 3.13.0 ドキュメント](https://docs.python.org/ja/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor)

補足: pythonでは GILを無効化させるオプションが追加されました。今回は試せていないですが、そちらを利用した場合、今回の結果は変わると思います（参考: 小門さん[GILを無効化したPythonを早速試してみた (2024/06 更新)](https://tech.revcomm.co.jp/benchmark-no-gil-with-python313a5))

### 5.2. Rustの所有権と型システムによる安全性

Rustは所有権と借用の概念を通じて、コンパイル時にデータ競合やメモリ安全性を保証します。このため、ランタイムでのロック管理が不要となり、`rayon`のようなライブラリが効率的に並列処理を行うことが可能です。また、Rustの高いパフォーマンスは、低レベルの最適化が可能な言語設計に起因しています。

- なぜRustは速いのか書かれた記事: [所有権とライフタイム](https://doc.rust-jp.rs/rust-nomicon-ja/ownership.html)
    - 所有権の存在意義について書かれた記事: @nirasanさん [Rust の所有権システムが難しくて一度挫折したあの日の自分のためにわかりやすい読み物があるとよいと思ったのだ。 #Rust - Qiita](https://qiita.com/nirasan/items/9e169859c6807c2c175b)

## 6. 最後に

並列処理を実装する際、Pythonはその簡潔さと豊富なライブラリにより、多くの場面で有用です。しかし、CPUバウンドなタスクや高いパフォーマンスが求められる場合、Rustは非常に強力な選択肢となります。Rustの高性能な並列処理ライブラリを活用することで、効率的かつ安全に並列処理を実現できます。

もし、高速な並列処理が必要なプロジェクトに取り組んでいるなら、PythonだけでなくRustの利用も検討してみてはいかがでしょうか。

今回並列処理と言っても、スレッドにおける極めて簡単な数値計算に注目をしましたがSIMDやGPUなどの他の並列手法やI/Oに関する並列の内容もあるため、興味があればそれらについても確認をしてみてはいかがでしょうか。

- SIMDに関して: [std::simd - Rust](https://doc.rust-lang.org/std/simd/index.html)
- GPUに関して: [EmbarkStudios/rust-gpu: 🐉 Making Rust a first-class language and ecosystem for GPU shaders 🚧](https://github.com/EmbarkStudios/rust-gpu)

## 7. おまけ

実は、今回の処理の場合はPythonの場合は並列実行しないほうが良いことがわかります
([rust-parallel-with-python/only_python.py at main · makinzm/rust-parallel-with-python](https://github.com/makinzm/rust-parallel-with-python/blob/main/only_python.py)を可視化したのが以下の内容です)。

![only_python_execution_times_comparison.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417525/4a37b710-8432-a21f-064f-cd6550995f6f.png)

おそらくオーバーヘッドの関係で並列処理をしないほうが良いということなのだと思います。

ただ、Pythonの並列処理をしていないものとRustの並列処理を比較しても、結局Rustのほうが速いことがわかります（[実験結果をPlotするスクリプト](https://github.com/makinzm/rust-parallel-with-python/blob/main/fast_plot.py)の実行内容)。

![fast_execution_times_comparison.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417525/dacfb9ae-dfd1-080c-f787-ffb8f4f20651.png)