Cython 演習問題 解説
演習1 素数判定
整数の簡単な演算を中心とした処理だが、このような処理を拡張モジュール化する場合の効果を確認してみよう。
まず、演習1-(A)のスクリプトをそのままCythonを使用して拡張モジュール化した場合では、
# Python版 $python -mtimeit "import prime;prime.primes(1000)" 10 loops, best of 3: 120 msec per loop
# 拡張モジュール版 $python -mtimeit "import prime1;prime1.primes(1000)" 10 loops, best of 3: 79 msec per loop
となり、処理時間は30%程度短くなっている。この高速化はどのようにもたらされているのだろうか。
Pythonはスクリプトをバイトコードに変換し、仮想スタックマシン上で実行するようになっている。たとえば
c = a + b
というスクリプトは
LOAD_FAST 0 (a) LOAD_FAST 1 (b) BINARY_ADD STORE_FAST 2 (c)
という4つの命令に変換され、インタープリタはこのバイトコードを先頭から順に実行する。
バイトコードの最初の命令は "LOAD_FAST 0 (a)" となっている。これは 変数aをロードしろという命令だということは何となく想像が付くが、ロードした値はどこに行ってしまうのだろうか? どこかに書き込まれるのだろうが、その書き込み先はどこにも指定されていない。
バイトコードを実行するときには、スタックというデータ領域が用意されていて、それぞれの命令はスタックに対してデータをプッシュ・ポップして必要なデータをやりとりしている。 "LOAD_FAST 0 (a)" は、変数表から変数aの値を取得し、スタックにプッシュする命令である。
次の "LOAD_FAST 1 (b)" も同様に変数bを取得し、スタックにプッシュする。
"BINARY_ADD" は、スタックの上から二つのデータをポップし、
加算の結果をスタックにプッシュする。
最後に、"STORE_FAST 2 (c)"はスタックからデータをポップし、変数表に値を設定する。
このように、Pythonインタープリタはバイトコードの読み込み、スタックのプッシュ/ポップを行いつつ処理を行っている。一方、Cythonではバイトコードもスタックマシンも使用せず、次のようなC言語のソースコードを生成して実行するだけである。
PyObject *c = PyNumber_Add(a, b)
Cythonでコンパイルしたコードはバイトコードの読み込みやスタックの操作などのオーバヘッドがなく、効率的に実行することができる。しかし、バイトコードの実行はかなり効率的に行われるので、単純にCythonで実装してもそれほど大きな変化は見込めない。この例のように、〜30%程度の改善がせいぜいである。
次に、演習1-(C)を見てみよう。
$ python -mtimeit "import prime1;prime1.primes(1000)" 100 loops, best of 3: 4.34 msec per loop
かなり大幅に高速化した。こちらでは1-(A)のスクリプトと違い、使用している変数はすべて int や long 型として宣言されている。この宣言により、処理中で使われている整数値は、Pythonの数値オブジェクトではなく C言語の整数型が使用されるようになるのである。
型を宣言せず、Pythonオブジェクトで計算を行う場合、スクリプト中の
i = i + 1
という文では次のような処理が行われる。
- 数値オブジェクト i と、値 1 の数値オブジェクトが加算可能か判定し、加算処理を行う関数を決定する。
- i の値と 1 を加算する。
- 加算の結果を格納する数値オブジェクトをヒープ上に作成する。
- 数値オブジェクト i を解放する
- 3.で作成したオブジェクトを新たに 変数 i として格納する。
一方、C言語の整数型を使えば
- 変数i に 1 を加える。
だけである。この処理はCPUが直接実行することのできる機械語の一命令だけで完結するので、Pythonオブジェクト版とは比較にならないほど高速に実行することができるのである。
演習2 型定義
演習2は、簡単な型定義の例である。演習1では元のPythonスクリプトをCython化するだけである程度パフォーマンスが向上したが、演習2ではループの回数が少ないため、インタープリタのオーバヘッドを除いただけではほとんど効果がない。
# Python版 $python -mtimeit "import bitset;bitset.BitSet(0x55).setflag(range(10000))" 100 loops, best of 3: 5.53 msec per loop
# Cython版 $python -mtimeit "import bitset;bitset2.BitSet(0x55).setflag(range(10000))" 100 loops, best of 3: 5.21 msec per loop
演習2-(B)では、演習1-(C)と同様に数値演算部分に型を宣言すると同時に、ループ中で呼び出すメソッドを "cdef" と宣言している。
cdef と宣言されたメソッドは def によるメソッドとは異なり、Pythonスクリプトから呼び出すことのできる通常のメソッドは生成しない。cdef なメソッドはC言語で直接呼び出すことのできる、C言語の関数となる。
Pythonのメソッドや関数を呼び出すためには、実行用のフレームオブジェクトの生成・解放や引数の解析など、かなり多くの処理が必要である。しかし、C言語の関数であればそういった処理は不要なため、大きなパフォーマンスの向上が期待できる。この例では、
$python -mtimeit "import bitset2;bitset2.BitSet(0x55).setflag(range(10000))" 1000 loops, best of 3: 506 usec per loop
と、処理速度は十倍高速になっている。