Pythonを高速化するCythonを試してみよう#
CythonはPython likeなコードをC/C++に変換、ネイティブコンパイルして高速動作するコンパイラです。
このノートではこのCythonのいくつかの違う設定をした関数と、pure Pythonで実装した関数との速度比較を行います。
%load_ext Cython
フィボナッチ数列#
pure python
def fib_python(n):
a, b = 0, 1
for i in range(n):
a, b = a + b, a
return a
cython(特に型アノテーションをつけていない状態)
%%cython
def fib_cython(n):
a, b = 0, 1
for i in range(n):
a, b = a + b, a
return a
cython (引数にだけ型アノテーションをつけた状態)
%%cython
def fib_typed_cython(int n):
a, b = 0, 1
for i in range(n):
a, b = a + b, a
return a
cython (関数内で使われている変数にも全て型アノテーションをつけた状態)
%%cython
def fib_all_typed_cython(int n):
cdef int a,b,i
a, b = 0, 1
for i in range(n):
a, b = a + b, a
return a
%timeit fib_python(5000)
%timeit fib_cython(5000)
%timeit fib_typed_cython(5000)
%timeit fib_all_typed_cython(5000)
280 µs ± 9.68 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
245 µs ± 5.06 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
224 µs ± 5.42 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
1.39 µs ± 15.4 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
import matplotlib.pyplot as plt
%matplotlib notebook
import seaborn as sns
sns.set()
plt.bar(["func1","func2","func3","func4"],[664,415,346,2.01])
<BarContainer object of 4 artists>
型付けをたくさん行った方が断然速くなる事がわかります。
特に最後の関数は桁違いの速度が出ています。
ですが引数のみに型アノテーションをつけた状態でも、素のPythonよりはかなり速いですし、cdefを使った型付けを大変だと思う人もいるかも知れません。
Cythonらしい書き方をしていくと、Pythonらしさが無くなっていく感じがあります。
書きやすさと速度がトレードオフになる感じですね。
ユークリッドの互除法(再帰関数版)#
最大公約数を求めるアルゴリズムです。
def gcd_python(x, y):
if y == 0:
return x
else:
return gcd_python(y, x % y)
%%cython
def gcd_cython(x, y):
if y == 0:
return x
else:
return gcd_cython(y, x % y)
%%cython
def gcd_typed_cython(int x, int y):
if y == 0:
return x
else:
return gcd_typed_cython(y, x % y)
%timeit gcd_python(10000,325)
%timeit gcd_cython(10000,325)
%timeit gcd_typed_cython(10000,325)
124 ns ± 4.19 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
50.7 ns ± 1.48 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
57.2 ns ± 0.749 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
plt.bar(["func1","func2","func3"],[785,445,396])
<BarContainer object of 3 artists>
こちらも順当に高速化できています。
ただしここまで試して、再帰関数と普通のループだとどちらが速いのか気になりました。
普通のwhile文で書き直したバージョンも試してみます。
ユークリッドの互除法(普通のwhile版)#
def gcd_python2(x,y):
while y!=0:
x, y = y, x% y
return x
%%cython
def gcd_cython2(x,y):
while y!=0:
x, y = y, x% y
return x
%%cython
def gcd_typed_cython2(int x,int y):
while y!=0:
x, y = y, x% y
return x
%timeit gcd_python2(10000,325)
%timeit gcd_cython2(10000,325)
%timeit gcd_typed_cython2(10000,325)
82.6 ns ± 2.73 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
36.1 ns ± 0.571 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
18.3 ns ± 0.107 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)
plt.bar(["func1","func2","func3"],[340,221,161])
<BarContainer object of 3 artists>
順当に高速化できています。
pure pythonの時点で再帰関数版よりも2倍速いです。
cython版は、1番最適化を行ったものが1番伸び率が大きいですね。
再帰関数は書きやすいですが、Python/Cythonで速度を気にする場合はやめたほうが良いかも知れませんね。