最近少しずつRustに手を出し始めています。
CPUバウンドとなっている重い処理をPythonから切り出してコンバートするユースケースを考えたときに、PythonからRustを呼び出すための仕組みが必要となります。
今回はPythonからRustバイナリを実行する (あるいはその逆) ためのバインディングを行うPyO3について、エラトステネスのふるいを実装しながら紹介します。
- 他の選択肢としてrust-cpythonもあるのですが、最近はPyO3の方がメジャーっぽいです
- 実行環境はMac OS X (10.14) を想定しています
パッケージの作成
ライブラリテンプレートを使ってeratosthenesパッケージを作成します。
加えてPyO3はnightlyバージョンのRust環境が必要ですので、インストールして切り替えています。
$ cargo new eratosthenes --lib $ cd eratosthenes $ rustup install nightly $ rustup default nightly $ rustup toolchain list stable-x86_64-apple-darwin nightly-x86_64-apple-darwin (default)
Cargo.tomlにPyO3の依存関係を追記します。ここでは0.9.0-alpha.1
を使っています。
... [lib] name = "eratosthenes" crate-type = ["cdylib"] [dependencies.pyo3] version = "0.9.0-alpha.1" features = ["extension-module"]
Rustの実装
src/lib.rsを編集し、エラトステネスのふるいを実装します。
- 最初の2行はPyO3で使うモジュールが宣言されてます
- pyo3::prelude以下にはこの後使うPyModuleやPyResultなどの基本モジュールが公開されています
#[pymodule]
アトリビュート付きのeratosthenesがPythonモジュール (import eratosthenes
) にバインディングされます- この後定義するget_prime_numbersをモジュールへ追加してます (
eratosthenes.get_prime_numbers(10)
で実行できます)
- この後定義するget_prime_numbersをモジュールへ追加してます (
#[pyfunction]
アトリビュート付きのget_prime_numbersがPython関数にバインディングされます- 返り値Vec
は、Pythonではintのlistとなります
- 返り値Vec
use pyo3::prelude::*; use pyo3::wrap_pyfunction; #[pymodule] fn eratosthenes(py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(get_prime_numbers))?; Ok(()) } #[pyfunction] fn get_prime_numbers(n: i32) -> PyResult<Vec<i32>> { let mut flags = Vec::new(); for _ in 0..n+1 { flags.push(true); } let upper = (n as f32).sqrt().floor() as i32; for i in 2..upper+1 { if !flags[i as usize] { continue; } let prime = i; let mut j = prime * 2; while j <= n { flags[j as usize] = false; j += prime; } } let mut primes = Vec::new(); for i in 2..n+1 { if flags[i as usize] { primes.push(i); } } Ok(primes) }
ビルド
releaseオプション付きでビルドすると、ダイナミックライブラリliberatosthenes.dylibが出力されます。
$ cargo build --release $ ls target/release build/ deps/ examples/ incremental/ liberatosthenes.dylib
Pythonから実行
liberatosthenes.dylibを、実行するPythonファイルと同じディレクトリにeratosthenes.soとしてコピー&リネームまたはシンボリックリンクさせると、Pythonから実行できます。
import eratosthenes primes = eratosthenes.get_prime_numbers(10) print(primes) # [2, 3, 5, 7]
速度の比較
試しにPythonでもエラトステネスのふるいを実装して速度をざっくり比較してみました。10000000の素数リストを10回計算してます。
import eratosthenes def get_prime_numbers(n: int): flags = [True for _ in range(n+2)] upper = int(n ** 0.5) for i in range(2, upper+1): if not flags[i]: continue prime = i j = prime * 2 while j <= n: flags[j] = False j += prime primes = [] for i in range(2, n+1): if flags[i]: primes.append(i) return primes if __name__ == "__main__": import sys use_rust = len(sys.argv) == 2 and sys.argv[1] == "--rust" n = 10000000 for _ in range(10): if use_rust: primes = eratosthenes.get_prime_numbers(n) else: primes = get_prime_numbers(n)
結果は以下の通りで、Rustが約40倍速い結果となりました。実行可能なバイナリなので当然と言えばそれまでですが、やっぱりすごい。
$ time python src/eratosthenes.py python src/eratosthenes.py 26.31s user 0.51s system 99% cpu 26.867 total $ time python src/eratosthenes.py --rust python src/eratosthenes.py --rust 0.64s user 0.17s system 96% cpu 0.834 total