Python
初心者
Cython

Cythonの書き方入門[備忘録]

More than 1 year has passed since last update.


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ループを二重にした以下のようなコードを使用する.


python_code.py

# -*-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_aarr_bの全組み合わせの和の合計を求めているだけ.

これを実行すると,


out[1]

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言語で実装する.ヘッダーファイルとソースコードは以下の通り.


cython_c_code.h

#ifndef CYTHON_CODE

#define CYTHON_CODE
int c_algo(int*, int*, int, int);
#endif


cython_c_code.c

#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に書いていく.


cython_pyx_code.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の書き方


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で動かしてみる


cython_py_code.py

# -*-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))



out[2]

999000000

Execution time:0.0010039806365966797 [sec]

およそ速度が245倍向上した.


ついでに

.pyxファイルに型を指定して直接コードを書くこともできる.


cy_only.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から呼び出すと,


out[3]

999000000

Execution time:0.10053086280822754 [sec]

となって大体2倍強速度は向上したが,Pythonのように手軽に書ける分Cで書くよりは遅いらしい.


コメントで指摘いただいたのでコードを以下のように修正してやってみた.



cy_only.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 = 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


すると,


out[4]

999000000

Execution time:0.0019919872283935547 [sec]

となりCで書いたものに近い速度となった.

Cythonベタ書きでもちゃんと書けば早くなることがわかった.


さいごに

まだCythonを触り始めたばかりで入門レベルしか理解できていないため適当なことを書いている可能性があります.その際にはご指摘いただけると幸いです

setup.pyで不要なオプションを無効にすることで速度がさらに向上するようです.

こちらに一覧があります.

もう少し理解できてきたらこの記事を見やすく書き直すかもしれません.