Rust で Python の拡張ライブラリ作成 と numpy との性能比較

Python
スポンサーリンク

この記事は 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のみが紹介されている。

Not applied. opt:{"url":"”https:\/\/www.reddit.com\/r\/rust\/comments\/6p3rjp\/pyo3_python_rust_binding\/”","description":"”I’d","0":"like","1":"to","2":"introduce","3":"[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-numpyrust-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秒かかっているので、速度で勝負するのは根本的に厳しそう。高速化できるのかな。

Github Repo

参考記事

RustでNumPyを拡張する – Qiita
はてブ数
Pythonの利点対話的にデータの加工、解析、可視化 on Jupyterグルー言語的用法公式・非公式によってPythonインターフェースはだいたい用意されてい...

コメント

タイトルとURLをコピーしました