Cythonによるパフォーマンス向上
- 作者: Kurt W. Smith,中田秀基,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/06/19
- メディア: 大型本
- この商品を含むブログ (3件) を見る
Pythonのような書きやすいインタープリタ型言語で書き始めて結果を取得できるところまでいくと、その後にはパフォーマンスを上げるためにC++で書きなおそうか、と思ったりする。かといって最初からC++で書いていては、きちんと動くものを書き切るのに間違いなく時間がかかるし、とにかくまず結果が欲しいときには取るべき有効な手段ではないと思う。
そこでCythonの出番。Pythonのコードはそのままで有効なCythonのコードでもあるので、cythonをインストールして拡張子をpyx
に変更し、import pyximport; pyximport.install()
をつけるだけで速度改善の効果が望める。もちろん、静的な型指定などのチューニングを施すことによって、Pythonのセマンティクスを保ったまま、よりCに近い速度を達成することができる。これだけ簡単に導入できるので、もしCPUバウンドな処理であることが分かっているなら、Cythonを使わない手はないだろう。
どのような場合にどの程度の速度改善が望めるか、以下のようにPythonのコードを実行した結果と、PythonのコードをそのままCythonとしてコンパイルして実行した結果、Cythonとして書き直したコードを実行した結果、を比較した。プログラムの内容は、単にソートを実施しているだけだが、再帰呼び出しの有り・無し、でそれぞれプログラムを変えている。Cythonでは、stdlib.h
のqsort
を使っている。
再帰呼び出し有り、では関数呼び出しのオーバーヘッドが大きくなるため、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]: