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で速度を気にする場合はやめたほうが良いかも知れませんね。