Cython を使ってみる

2016年2月14日

はじめに

Python コードを C に変換してコンパイルする Cython について、Micha Gorelick, Ian Ozsvald 著 "ハイパフォーマンス Python" の内容に沿って試してみたメモ。

環境

  • Intel Core i5 M450 2.4 GHz
  • Windows 7 64 bit
  • MSYS2 64 bit (MinGW w64)
  • Anaconda for Windows (Python 2.7)
  • Microsoft Visual C++ Compiler for Python 2.7

※MinGW にも Python が入っているので、Anaconda のほうを使うようにパスを設定している。

Cython は Anaconda に含まれている。バージョンは 0.23.4。

サンプルコード

サンプルコードは こちら から入手。

ここでは以下のものを用いる。

07_compiling/cython/lists/1/

  • julia1.py
  • cythonfn.pyx
  • setup.py

Cython によるコンパイル

Cython によるコンパイルは次の手順で行う。

      Python コードからコンパイルしたい部分をモジュールとして分離する。拡張子は ".pyx" とする。
      Cython 用に必要な注釈を追記する。
      setup.py でコンパイルする。

setup.py の中身は以下の通り。

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    cmdclass={'build_ext': build_ext},
    ext_modules=[Extension("calculate", ["cythonfn.pyx"])]
)

次のようにコンパイルする。

$ python setup.py build_ext --inplace

オプションの "--inplace" は、モジュールをカレントディレクトリに生成するように指示するもの。うまく行けば、Windows の場合は ".pyd" ファイルが、Linux の場合は ".so" ファイルができる。

サンプルを実行すると、次のようなエラーが出た。

$ python setup.py build_ext --inplace
running build_ext
cythoning cythonfn.pyx to cythonfn.c
building 'calculate' extension
error: Unable to find vcvarsall.bat

これは、Visual C++ の環境を探したけど見つからなかったことを意味している。ここでは、以下のコンパイル環境を試してみる。

  • Microsoft Visual C++ Compiler for Python 2.7
  • MinGW

Microsoft Visual C++ Compiler for Python 2.7

Microsoft が こちら に Python 用のコンパイル環境を用意してくれているので、これを導入する。

インストールすると、なぜかユーザーフォルダの "AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0" にインストールされる。インストールしただけでは Python が認識してくれないので、Python フォルダの "Lib\distutils\msvc9compiler.py" に設定を追記する。このファイルの関数 find_vcvarsall() の最後の "if not productdir" の前に、以下のように productdir に Visual C++ for Python のパスを設定する。※(ユーザー名) は自分のユーザー名に置き換えること。

def find_vcvarsall(version):
    ...
    
    productdir = "C:\\Users\\(ユーザー名)\\AppData\\Local\\Programs\\Common\\Microsoft\\Visual C++ for Python\\9.0"
    
    if not productdir:
        log.debug("No productdir found")
        return None
	...

MinGW

MinGW を用いる場合は、Python フォルダの "Lib\distutils" に "distutils.cfg" という名前で以下の内容のファイルを置いておく。

[build]
compiler = mingw32

これだけだとライブラリをリンクできないので、以下のように MinGW でライブラリを変換する。

$ gendef /c/Anaconda2/python27.dll
$ dlltool -d python27.def -l libpython27.dll.a
$ cp libpython27.dll.a /c/Anaconda2/libs/

VC++ でも MinGW でも好きな方でよいのだが、line_profiler を使う場合は、line_profiler が VC++ を使っているため、VC++ を使わざるを得ない。

Cython による最適化

もとのコードの計算結果は以下の通り。

$ python julia1_nopil.py
Length of x: 1000
Total elements: 1000000
calculate_z_serial_purepython took 12.013999939 seconds

Cython でコンパイルした結果は以下の通り。

$ python julia1.py
Length of x: 1000
Total elements: 1000000
Took 6.93200016022 seconds
Total sum of elements (for validation): 33219980

確かに速くなっている。

文献では、もとのコードに "cdef" で変数の型情報を追加すると速くなるとある。

def calculate_z(maxiter, zs, cs):
    """Calculate output list using Julia update rule"""
    cdef unsigned int i, n
    cdef double complex z, c
    output = [0] * len(zs)
    for i in range(len(zs)):
        n = 0
        z = zs[i]
        c = cs[i]
        while n < maxiter and abs(z) < 2:
            z = z * z + c
            n += 1
        output[i] = n
    return output

結果。

$ python julia1.py
Length of x: 1000
Total elements: 1000000
Took 7.83299994469 seconds
Total sum of elements (for validation): 33219980

遅くなった。文献ではさらに、abs() を直接計算に置き換えている。

        while n < maxiter and (z.real*z.real + z.imag*z.imag) < 4:

結果。

$ python julia1.py
Length of x: 1000
Total elements: 1000000
Took 3.34500002861 seconds
Total sum of elements (for validation): 33219980

確かにだいぶ速くなったが、文献ほどではない。

Cython の変換情報

Cython では、以下のようにして Python を C に変換できる。

$ cython -a cythonfn.pyx

これにより、変換情報の HTML ファイルが生成される。

黄色が濃い行ほど、Python の呼び出しが多いことを意味しており、最適化の余地がありそうなことを示している。

Jupyter Notebook における Cython の利用

Jupyter Notebook では、もっと簡単に Cython を利用できる。まず、次のコマンドで Cython 拡張を読み込む。

%load_ext Cython

"%%cython" を付けて Cython のコードを書いて実行する。

%%cython

def calculate_z(maxiter, zs, cs):
    """Calculate output list using Julia update rule"""
    output = [0] * len(zs)
    for i in range(len(zs)):
        n = 0
        z = zs[i]
        c = cs[i]
        while n < maxiter and abs(z) < 2:
            z = z * z + c
            n += 1
        output[i] = n
    return output

"%%cython" の代りに "%%cython --annotate" とすれば、Cython の変換情報が得られる。

参考文献