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

> 2017/08/28: コメントで指摘いただいた箇所の修正を行いました．

# 概要
最近Pythonでの遅い処理をCythonを用いてCで実装して速度を上げようと試みたので，やり方を忘れたときに見返せるようにまとめておきます．
**あくまで超初歩で表面的なことしか書いてありません**

# Cythonとは
CythonはCで書かれている関数をPythonから呼び出すことのできるように仲介するようなものであるという認識をしています．
つまり実行の遅い部分をCで実装し，それ以外をPythonで簡単に描くといった要領でそれぞれのいい所を利用できるといったイメージです．

# インストール方法
## windows
[このサイト](http://www.lfd.uci.edu/~gohlke/pythonlibs/#cython "cython")にアクセスし対応するバージョンのものをダウンロードします．（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/#VC-2)を参考にすると解決しました
>
>参考サイト
>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ループを二重にした以下のようなコードを使用する．

``` py3: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_a`と`arr_b`の全組み合わせの和の合計を求めているだけ．
これを実行すると，

```:out[1]
999000000
Execution time:0.24517321586608887 [sec]
```
となる．

# C言語で書く内容
Pythonはforループに時間がかかっているらしいので，

```python
for elem_a in arr_a:
        for elem_b in arr_b:
            res = res + elem_a + elem_b
```
この部分をC言語で実装する．ヘッダーファイルとソースコードは以下の通り．

```c:cython_c_code.h
#ifndef CYTHON_CODE
#define CYTHON_CODE
int c_algo(int*, int*, int, int);
#endif
```

```c: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: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のヘッダーファイルから使用する関数を記述する．

```cython
cdef extern from ヘッダーファイル名:
    使用する関数
```
このような形で記述する．

最後にPython側から呼び出す関数を記述する．
Pythonのように`def`で記述するが，引数の部分に型を指定する．
numpy配列を引数として受け取る時の型は`np.ndarray[要素の型, ndim=次元数]`とする．
また，Cの関数に配列のポインタを渡す際には`&配列名[0]`で渡すことができる．

# setup.pyの書き方

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

```py3: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ファイルに型を指定して直接コードを書くこともできる．

```cython: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で書くよりは遅いらしい．

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

```cython: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`で不要なオプションを無効にすることで速度がさらに向上するようです．
[こちら](http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiler-directives)に一覧があります．
もう少し理解できてきたらこの記事を見やすく書き直すかもしれません．