14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonAdvent Calendar 2024

Day 3

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

Last updated at Posted at 2024-12-02

1. 目的

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

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

2. Pythonの並列処理

Pythonでは、標準ライブラリとしてconcurrent.futuresモジュールが提供されており、その中でもThreadPoolExecutorはスレッドベースの並列処理を簡単に実装できる便利なツールです。以下は、ThreadPoolExecutorを用いた簡単な並列処理ので,0からn-1までの数値の二乗を計算する関数を並列に実行し配列に格納しています。

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クレートを利用することで簡潔かつ効率的に並列処理を実装できます。

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

rayonはデータ並列処理を簡単に実現できるクレートであり、スレッドプールの管理やタスクの分割を自動的に行ってくれます。以下は、rayonを用いた並列処理のです。

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を用いて並列処理を行うことも可能ですが、スレッドの生成や共有データの管理は手動で行う必要があり、コードが複雑化しがちです。例えば、以下のような実装はosレベルのスレッドの生成数が多くなり悪化します。

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に限らず、並列処理全般に言えることです)。

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

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

4. 比較

Rustのコードをmaturinを用いてPythonのモジュールとしてビルドし、Pythonから呼び出すことで、両者のパフォーマンスを比較しました(詳細なコード)。

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

maturinの利用については本ドキュメントでは扱いませんが非常に簡単にできるためmaturinの公式ドキュメント上記の詳細コードのREADME.mdなどを参考にしてください。

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

execution_times_comparison.png

5. 考察

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

5.1. PythonのGILの影響

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

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

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

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

6. 最後に

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

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

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

7. おまけ

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

only_python_execution_times_comparison.png

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

ただ、Pythonの並列処理をしていないものとRustの並列処理を比較しても、結局Rustのほうが速いことがわかります(実験結果をPlotするスクリプトの実行内容)。

fast_execution_times_comparison.png

14
5
0

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
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?