Cythonによるパフォーマンス向上

Cython ―Cとの融合によるPythonの高速化

Cython ―Cとの融合によるPythonの高速化

Pythonのような書きやすいインタープリタ型言語で書き始めて結果を取得できるところまでいくと、その後にはパフォーマンスを上げるためにC++で書きなおそうか、と思ったりする。かといって最初からC++で書いていては、きちんと動くものを書き切るのに間違いなく時間がかかるし、とにかくまず結果が欲しいときには取るべき有効な手段ではないと思う。

そこでCythonの出番。Pythonのコードはそのままで有効なCythonのコードでもあるので、cythonをインストールして拡張子pyxに変更し、import pyximport; pyximport.install()をつけるだけで速度改善の効果が望める。もちろん、静的な型指定などのチューニングを施すことによって、Pythonのセマンティクスを保ったまま、よりCに近い速度を達成することができる。これだけ簡単に導入できるので、もしCPUバウンドな処理であることが分かっているなら、Cythonを使わない手はないだろう。

どのような場合にどの程度の速度改善が望めるか、以下のようにPythonのコードを実行した結果と、PythonのコードをそのままCythonとしてコンパイルして実行した結果、Cythonとして書き直したコードを実行した結果、を比較した。プログラムの内容は、単にソートを実施しているだけだが、再帰呼び出しの有り・無し、でそれぞれプログラムを変えている。Cythonでは、stdlib.hqsortを使っている。

再帰呼び出し有り、では関数呼び出しのオーバーヘッドが大きくなるため、PythonのコードをそのままCythonとしてコンパイルして実行した結果、が、Pythonのコードを実行した結果に比べて、速度改善率が高い。

再帰呼び出し無し、では、ソートするリストの長さを1000倍しており、Cythonのコードの速度改善率がかなり高い。

このようにある特定のモジュールをCython化するだけでも、かなり速度改善が望めることが分かる。Cそのもののコードを書かずにPythonスクリプトの書きやすさを保ったままパフォーマンス向上が可能となる。あとは、プロファイラーを使ってCython化すべき箇所を特定する作業が必要になるだろう。

再帰呼び出し有り

$ cat pyqsort.py 
def pyqsort(x):
    if len(x)==1:
        return
    array = sorted(x)
    pyqsort(x[:-1])
$ cat cyqsort.pyx 
def cyqsort(x):
    if len(x)==1:
        return
    array = sorted(x)
    cyqsort(x[:-1])
$ cat cqsort.pyx 
cdef extern from "stdlib.h":
    void qsort(void *array,size_t count,size_t size,int (*compare)(const void*,const void*))
    void *malloc(size_t size)
    void free(void *ptr)

def cqsort(list x):
    cdef:
        int *array
        int i, N
    
    N = len(x)
    if N==1:
        return
    array = <int*>malloc(sizeof(int) * N)
    if array == NULL:
        raise MemoryError("Unable to allocate array.")

    for i in range(N):
        array[i] = x[i]
        
    qsort(<void*>array, <size_t>N, sizeof(int), int_compare)
   
    free(array)

    cqsort(x[:-1])

cdef int int_compare(const void *a, const void *b):
    cdef int ia, ib
    ia = (<int*>a)[0]
    ib = (<int*>b)[0]
    return ia-ib
$ ipython --no-banner

In [1]: from pyqsort import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(100))

In [4]: shuffle(x)

In [5]: %timeit pyqsort(x)
1000 loops, best of 3: 642 µs per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x106dc1eb8>)

In [7]: from cyqsort import cyqsort

In [8]: %timeit cyqsort(x)
1000 loops, best of 3: 555 µs per loop

In [9]: from cqsort import cqsort

In [10]: %timeit cqsort(x)
1000 loops, best of 3: 349 µs per loop

In [11]: 

再帰呼び出し無し

$ cat pyqsort_once.py 
def pyqsort(x):
    array = sorted(x)
$ cat cyqsort_once.pyx
def cyqsort(x):
    array = sorted(x)
$ cat cqsort_once.pyx
cdef extern from "stdlib.h":
    void qsort(void *array,size_t count,size_t size,int (*compare)(const void*,const void*))
    void *malloc(size_t size)
    void free(void *ptr)

def cqsort(list x):
    cdef:
        int *array
        int i, N
    
    N = len(x)
    if N==1:
        return
    array = <int*>malloc(sizeof(int) * N)
    if array == NULL:
        raise MemoryError("Unable to allocate array.")

    for i in range(N):
        array[i] = x[i]
        
    qsort(<void*>array, <size_t>N, sizeof(int), int_compare)
   
    free(array)

cdef int int_compare(const void *a, const void *b):
    cdef int ia, ib
    ia = (<int*>a)[0]
    ib = (<int*>b)[0]
    return ia-ib
$ ipython --no-banner

In [1]: from pyqsort_once import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(1000000))

In [4]: shuffle(x)

In [5]: %timeit pyqsort(x)
1 loop, best of 3: 760 ms per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x10c9bbc88>)

In [7]: from cyqsort_once import cyqsort

In [8]: %timeit cyqsort(x)
1 loop, best of 3: 759 ms per loop

In [9]: from cqsort_once import cqsort

In [10]: %timeit cqsort(x)
1 loop, best of 3: 193 ms per loop

In [11]: 

ループで呼び出し

$ ipython --no-banner

In [1]: from pyqsort_once import pyqsort

In [2]: from random import shuffle

In [3]: x=list(range(1000))

In [4]: shuffle(x)

In [5]: %timeit for i in range(1000): pyqsort(x)
1 loop, best of 3: 218 ms per loop

In [6]: import pyximport; pyximport.install()
Out[6]: (None, <pyximport.pyximport.PyxImporter at 0x10dd583c8>)

In [7]: from cyqsort_once import cyqsort

In [8]: %timeit for i in range(1000): cyqsort(x)
1 loop, best of 3: 218 ms per loop

In [9]: from cqsort_once import cqsort

In [10]: %timeit for i in range(1000): cqsort(x)
10 loops, best of 3: 84.8 ms per loop

In [11]: