2017/08/28: コメントで指摘いただいた箇所の修正を行いました.
概要
最近Pythonでの遅い処理をCythonを用いてCで実装して速度を上げようと試みたので,やり方を忘れたときに見返せるようにまとめておきます.
あくまで超初歩で表面的なことしか書いてありません
Cythonとは
CythonはCで書かれている関数をPythonから呼び出すことのできるように仲介するようなものであるという認識をしています.
つまり実行の遅い部分をCで実装し,それ以外をPythonで簡単に描くといった要領でそれぞれのいい所を利用できるといったイメージです.
インストール方法
windows
このサイトにアクセスし対応するバージョンのものをダウンロードします.(cp○○の部分がバージョンでPython3.6なら36となっているものを選びます)
コマンドプロンプトにpip install ダウンロードしてきたファイルのパス
とするとインストールが開始されます.
この先で
error: Unable to find vcvarsall.bat
などというエラーが出たら下記参考サイトの[別バージョンのVC++を使う設定]を参考にすると解決しました参考サイト
https://www.regentechlog.com/2014/04/13/build-python-package-on-windows/
Macやubuntuなど
$ pip install cython
でインストールできるかと思います.
Cをwrapするのに必要なファイル
- .cファイル: C言語での実装が書いてあるファイル
- .hファイル: C言語のヘッダーファイル
- .pyxファイル: C言語の関数などを使用してPythonから呼び出せる関数を書くファイル
- setup.py: コンパイルをするためのファイル
今回作るもの
早くなっていることを実感するために今回はforループを二重にした以下のようなコードを使用する.
# -*-encode: utf-8-*-
import time
if __name__ == "__main__":
start_t = time.time()
arr_a = [i for i in range(1000)]
arr_b = [i for i in range(1000)]
res = 0
for elem_a in arr_a:
for elem_b in arr_b:
res = res + elem_a + elem_b
print(res)
all_time = time.time() - start_t
print("Execution time:{0} [sec]".format(all_time))
やっていることは配列arr_a
とarr_b
の全組み合わせの和の合計を求めているだけ.
これを実行すると,
999000000
Execution time:0.24517321586608887 [sec]
となる.
C言語で書く内容
Pythonはforループに時間がかかっているらしいので,
for elem_a in arr_a:
for elem_b in arr_b:
res = res + elem_a + elem_b
この部分をC言語で実装する.ヘッダーファイルとソースコードは以下の通り.
#ifndef CYTHON_CODE
#define CYTHON_CODE
int c_algo(int*, int*, int, int);
#endif
#include "cython_c_code.h"
int c_algo(int *arr_a, int *arr_b, int size_a, int size_b){
int res = 0;
for(int i=0; i < size_a; i++){
for(int j=0; j < size_b; j++){
res = res + arr_a[i]+arr_b[j];
}
}
return res;
}
pyxファイルの書き方
上記のCの関数c_algo()
をPythonで呼び出せるように.pyxに書いていく.
cimport numpy as np
cdef extern from "cython_c_code.h":
int c_algo(int *arr_a, int *arr_b, int size_a, int size_b)
def cy_algo(np.ndarray[int, ndim=1] arr_a, np.ndarray[int, ndim=1] arr_b):
return c_algo(&arr_a[0], &arr_b[0], len(arr_a), len(arr_b))
ここで出てきたcimport
はcython版のヘッダーファイルを読み込む命令である.
つまりpython cimport numpy as np
と記述することで,np.ndarray
などの型の宣言が可能になる.
これとは別にnumpyの関数も必要ならばimport numpy
も必要.
↑コメントをもとに修正
次にCのヘッダーファイルから使用する関数を記述する.
cdef extern from ヘッダーファイル名:
使用する関数
このような形で記述する.
最後にPython側から呼び出す関数を記述する.
Pythonのようにdef
で記述するが,引数の部分に型を指定する.
numpy配列を引数として受け取る時の型はnp.ndarray[要素の型, ndim=次元数]
とする.
また,Cの関数に配列のポインタを渡す際には&配列名[0]
で渡すことができる.
setup.pyの書き方
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
from Cython.Build import cythonize
import numpy as np
sourcefiles = ['cython_pyx_code.pyx','cython_c_code.c']
setup(
cmdclass = {'build_ext': build_ext},
ext_modules = [Extension("cython_code", sourcefiles, include_dirs=[np.get_include()])],
)
必要なライブラリをimportしてsetup()
の引数ext_modules
には次のように記述する.
[Extension("このライブラリの名前", [使用するソースファイル類], include_dirs=[使用しているライブラリのヘッダ])]
今回はnumpyを使用しているのでnp.get_include()
をinclude_dirs
に渡す.
コンパイル
$ python setup.py build_ext -i
でコンパイルをする.
うまくいくとbuildディレクトリができ,さらにライブラリ名.so
(windowsなら.pyd
)ができている.これをふつうのPythonライブラリのようにimportして使用できる.
Pythonで動かしてみる
# -*-encode: utf-8-*-
import time
import numpy as np
import cython_code
if __name__ == "__main__":
start_t = time.time()
arr_a = [i for i in range(1000)]
arr_b = [i for i in range(1000)]
res = cython_code.cy_algo(np.array(arr_a), np.array(arr_b))
print(res)
all_time = time.time() - start_t
print("Execution time:{0} [sec]".format(all_time))
999000000
Execution time:0.0010039806365966797 [sec]
およそ速度が245倍向上した.
ついでに
.pyxファイルに型を指定して直接コードを書くこともできる.
cimport numpy as np
def cy_algo(np.ndarray[int, ndim=1] arr_a, np.ndarray[int, ndim=1] arr_b):
cdef int res
cdef int elem_a
cdef int elem_b
res = 0
for elem_a in arr_a:
for elem_b in arr_b:
res = res + elem_a +elem_b
return res
このように使用する変数すべてをcdef 型名 変数名
で定義する.
最初elem_a, elem_b
を定義し忘れたら,実行速度が遅くなってしまったので注意.
これをコンパイルしてPythonから呼び出すと,
999000000
Execution time:0.10053086280822754 [sec]
となって大体2倍強速度は向上したが,Pythonのように手軽に書ける分Cで書くよりは遅いらしい.
コメントで指摘いただいたのでコードを以下のように修正してやってみた.
cimport numpy as np
def cy_algo(np.ndarray[int, ndim=1] arr_a, np.ndarray[int, ndim=1] arr_b):
cdef int res = 0
cdef size_t len_a = len(arr_a)
cdef size_t len_b = len(arr_b)
for i in range(len_a):
for j in range(len_b):
res = res + arr_a[i] +arr_b[j]
return res
すると,
999000000
Execution time:0.0019919872283935547 [sec]
となりCで書いたものに近い速度となった.
Cythonベタ書きでもちゃんと書けば早くなることがわかった.
さいごに
まだCythonを触り始めたばかりで入門レベルしか理解できていないため適当なことを書いている可能性があります.その際にはご指摘いただけると幸いです
setup.py
で不要なオプションを無効にすることで速度がさらに向上するようです.
こちらに一覧があります.
もう少し理解できてきたらこの記事を見やすく書き直すかもしれません.