この記事は Python Advent Calendar 5日目の記事です。遅れてすみません。
Rust で Python の拡張ライブラリを書く
なぜRustなのか
この当たりを書いていると時間が無くなりそうなので、割愛したい。ただ、C++に馴染みのあるユーザーで高速に動作するコードを手っ取り早く書いていくにはRustが良さそうであるというのが自分なりの理解。特に、パッケージマネジャー当たりの優秀さはC++にはなくとても助かりそうである。メモリマネジメントもC++よりストレスレス。
PythonのAdventでRustなの?
Pythonのヘビーユースしだすと、C/C++ Extention を書いたり、Cythonなどを使って高速化していくことは特別なことではなくなる。そのひとつの選択肢として、これからはRustが良さそうだということで、勉強を兼ねて今回の記事を書くことにした。私自身はRustの初心者なので、間違っているところ等があればぜひご指摘いただきたい。
Rustのインストール
# install Rust
curl https://sh.rustup.rs -sSf | sh
Linux / MacOS ユーザーには上記コマンドで一発でインストールできる。Windowsユーザーはインストーラーを使ってインストールするが、RustはWindowsでは結構難があるので、その当たりは注意してほしい。
Rust コンパイル用 setuptools-rust をインストール
pip install setuptools-rust
Rustをpythonのsetuptoolsを使ってコンパイルできるライブラリ。python setup.py を使って、rustのライブラリを作成できるようになる。
Python Rust Extention
どうやら2本ある様子。他の記事ではだいたい、rust-cpython
を紹介しているのが多い。
rust-cpython
PyO3
比較
rust-cpython custom class
- rust-cpython uses whole new language based on macros
- it is very hard to extend py_class! macros, for example async/await support.
- generated functions are used for access to struct attributes.
- To drop PyObject GIL is required.
pyo3 custom class
- use proc_macro and specialization for class implementation (nightly is required)
- pyo3 does not modify rust struct. it is possible to define needed traits and make rust type compatible with python class without using #[py::class] macro.
- class customization is based on specialization and traits and associated types. separate trait is defined for each type
- of customization (i.e. PyMappingProtocol, PyNumberProtocol). macro is used for simplicity, but it is possible to make rust type compatible with specific customization without using proc macros.
- pyo3 does not inc ref for borrowed ptrs, PyObject does not need GIL to drop.
- pyo3 at least 15% faster.
pyo3 のが速い(イケてる)と書いているので、 pyo3 を使っていきたかったが、rust-numpyがrust-cpythonにしか対応していないので、rust-cpythonを使う。githubスターはrust-cpthonのほうが多い。setuptools-rustはPyO3の開発チームによるもので、READMEではpyo3のみが紹介されている。
プロジェクト作成
mkdir rust_binding_4_python
cd rust_binding_4_python
cargo new pyext-example
どうやら、rust-cpythonは2.7では現状動かない様子。Python3 に環境を変更する。
pyenv local miniconda3-4.1.11
Cargo.toml
[package]
name = "pyext-example"
version = "0.1.0"
authors = ["fx_kirin"]
[dependencies]
numpy = { git = "https://github.com/termoshtt/rust-numpy" }
cpython = "0.1"
ndarray = "0.10"
[lib]
crate-type = ["cdylib"]
Rustのライブラリの作成を行うので、必要dependenciesをセットしてください。rust-numpy
はgithubから直接取得しておかないとちゃんと動かなかった。
lib.rc
#[macro_use]
extern crate cpython;
extern crate numpy;
extern crate ndarray;
use numpy::*;
use ndarray::*;
use cpython::{PyResult, Python, PyObject};
/* Pure rust-ndarray functions */
// immutable example
fn sum(x: ArrayViewD<f64>) -> f64 {
x.scalar_sum()
}
// wrapper of `sum`
fn sum_py(py: Python, x: PyArray) -> PyResult<f64> {
let x = x.as_array().into_pyresult(py, "x must be f64 array")?;
let result = sum(x);
Ok(result) // Python function must returns
}
/* Define module "rust_binding" */
py_module_initializer!(rust_binding, init_rust_binding, PyInit_rust_binding, |py, m| {
m.add(py, "__doc__", "Rust extension for NumPy")?;
m.add(py, "sum", py_fn!(py, sum_py(x: PyArray)))?;
Ok(())
});
rust-numpy
はrust-ndarray
に変換してくれるので、sum functionを呼んでみました。rustの拡張は引数とか返り値の設定とかを都度読み込みの設定をしなくていいので楽ですね。
setup.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
from setuptools_rust import Binding, RustExtension
setup(name='pyext',
version='0.1.0.0',
rust_extensions=[
RustExtension('pyext.rust_binding',
'pyext-example/Cargo.toml', binding=Binding.RustCPython)],
packages=['pyext'],
# rust extensions are not zip safe, just like C-extensions.
zip_safe=False)
Build python package
python setup.py develop
性能比較
$ python main.py
## benchmarker: release 4.0.1 (for python)
## python version: 3.5.2
## python compiler: GCC 4.4.7 20120313 (Red Hat 4.4.7-1)
## python platform: Linux-4.4.0-101-generic-x86_64-with-debian-stretch-sid
## python executable: /home/zenbook/.pyenv/versions/miniconda3-4.1.11/bin/python
## cpu model: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz # 3924.609 MHz
## parameters: loop=100000, cycle=1, extra=0
## real (total = user + sys)
numpy.sum 0.2475 0.2500 0.2500 0.0000
faster_numpy.clibrary.sum 0.1182 0.1200 0.1200 0.0000
rust_binding.sum 1.7960 1.7800 1.7800 0.0000
## Ranking real
faster_numpy.clibrary.sum 0.1182 (100.0) ********************
numpy.sum 0.2475 ( 47.7) **********
rust_binding.sum 1.7960 ( 6.6) *
## Matrix real [01] [02] [03]
[01] faster_numpy.clibrary.sum 0.1182 100.0 209.5 1519.9
[02] numpy.sum 0.2475 47.7 100.0 725.5
[03] rust_binding.sum 1.7960 6.6 13.8 100.0
結構頑張ったけど、結局自作のnumpy用高速計算パッケージが一番早くて、numpyの標準ライブラリにも及ばない感じ。軽く調べたところ、rust-numpy
のデータ変換だけで0.6秒かかっているので、速度で勝負するのは根本的に厳しそう。高速化できるのかな。
コメント