【Cython】Pythonの高速化に挑戦!


はじめに

普段、Pythonを触っていて処理がもうちょっと速くなればなぁとことありませんか?

自分は、多次元ベクトルの類似度を計算する際に概算で50時間ということがありました。そこで、書きやすいPythonと処理が速いC言語を合わせたCythonに興味を持ったという感じです。

この記事では、Cythonを簡単に触れるという点を重視して説明したいと思います。
もっとこうしたら速くなるっていうのがあれば、コメントお願いします。

導入

今回は、jupyterを使っていこうと思います。

jupyterにはマジックコマンドと言われる(%や%%から始まる)コマンドがいくつか存在する。その中に、Cythonをコンパイルすることができるコマンドがあります。jupyter有能。

環境

  • OSX
  • jupyter ver4.2
  • Cython ver0.26.1

実際に触ってみる

フィボナッチ数列

よくあるフィボナッチ数列で簡単にCythonに触れてみます。

まずは純pythonでの実装。
jupyterのマジックコマンドを使って時間計測する。

これがCythonを使ってどのくらい速くなるのかというのが気になるとこです。

まず、Cythonを使う上でJupyter上で%load_ext Cythonを実行します。

そして、Cythonでコンパイルするためには%%cythonというマジックコマンドを宣言したセルで実装します。

ポイントはcdefによる静的な型宣言です。計算で用いてるa,bの型宣言はもちろんですが、for文で用いるiにも宣言をするのが重要なようです。

たった、これだけで16.5msという速さになりました。

次に、もう少し具体的な計算でCythonを触っていこうと思います。

コサイン類似度

フィボナッチ数列より少し複雑な計算で検証していきます。

対象のデータは、1ユーザーあたり100次元のベクトルを持つデータとします。これを5000ユーザー×1000ユーザーのデータ量に対して計算します。

フィボナッチ数列同様、純Pythonでの実装から見ていきます。

純Python

リストにインデックスでアクセスしたりと、かなり雑な実装になっていますが気にしないでください。

このrun2関数を実行すると、

この計測時間がベースラインになります。

Python + Cython

cdefで静的な型宣言したバージョン。
listで宣言するとappendが速くなったりするようです。

Cythonでコンパイルした関数でもdefで宣言しているなら、Pythonから呼び出すことができます。
なのですが、あえて関数wrap1でラップしています。

実行時間は以下です。

40秒ほど縮まりました。思ったほど、速くなりませんでしたね、、。

少し最適化しようと思います。

Cython 最適化①

最適化の1歩目として、関数のくくりだしやってみます。

def関数内ではPythonオブジェクトが介入するため、遅くなるようです。なので、cdef関数を使ってみます。静的な型宣言ではなく、関数の宣言で使います。

cdef関数はPythonから呼び出すことができなくなります。ですが、計算にPythonオブジェクトが介入しなくなるため処理が速くなようです。

今回は、単純な計算処理(乗算,除算)をcdef関数でくくりだしてみました。
cdef関数はCython内のdef関数から呼び出すことが可能です。

たったこれだけ、本当に速くなるのか、、。

だいぶ速くなった。

Cython 最適化②

今度は、cpdef関数を使ってみます。cython内ではcdef関数はCython内のdef関数から呼び出せますが、Pythonからは呼び出すことができません。一方、Cython内のdef関数はPythonから呼び出すことができます。

そうなると、cdefで定義した関数に対してPythonから呼び出すことができるCython内のdef関数が1つは必要になります。

いちいちラッピング用の関数を用意するのは面倒ということことから、cpdef関数が便利になります。
cpdef関数は、cdef関数とdef関数を混ぜたような関数だそうです。

ということで、今まで分けてたcython内のdef関数とcdef関数をcpdef関数にまとめてみます。

実行結果が以下。

期待が外れましたね。しかし、Python+Cythonと同じ実装でもcpdef関数での実行の方が多少速くなっています。このことから、cpdef関数の方がCython内def関数よりもアクセスが速いのかもしれません。

ということで、最後にcpdef関数での宣言とcdef関数のくくりだしをやってみます。

Cython 最適化③

実行結果が以下。

若干、変わりましたけど誤差ですかね。
cpdef関数は、戻り値と引数にも静的な型宣言が行えるためdef関数より速くなりそうな気がしてたんですが、、、。

Cython 最適化④

Cythonでは、C言語のライブラリを使うことができます。
コードを見直してみるとnumpyを使っている部分があります。pythonのライブラリをC言語のライブラリに変えてみたら速くなるのではと思いやってみました。


実行結果が以下。

ちょっと速くなった。できるだけC言語のライブラリを使った方がいい感じですね。

Python + Cython + 並列処理

試しに、このコードで並列処理してみました。
ちなみにMacbook Proなので1コア2スレッドです。

順調に速くなりましたね。

追記(11/20)

コメントいただいたので、C言語ライブラリを使った実行結果と除算に関するコンパイラディレクティブを追加した実行結果を追加します。

pow関数

cdef関数でc_powとしていたのですが、これをfrom libc.math import powでやってみました。



やはり、pythonのライブラリからC言語のライブラリに変えたというのが上記では大きかったことがわかりました。

cdivision=True

つぎに、コンパイラディレクティブで#cython: cdivision=Trueを追加したものです。
コンパイラディレクティブとはCython のコードの挙動に影響を及ぼす命令というふうに公式ドキュメントに書いてありました。

#cython: cdivision=Trueの効果としては、

False にセットすると、Cython は C の型に対する除算・剰余演算子に関 する仕様を、(被演算子間の符号が異なる場合の振る舞いが異なる) Python の int の仕様に合わせ、除算する数が 0 の場合に ZeroDivisionError を送出します。この処理を行わせると、速度に 35% ぐらいのペナルティが生じます。 True にセットすると、チェックを 行いません。

と書いていありました。



最適化④に比べて1秒ほど縮みました。
cythonを使う上でコンパイラディレクティブにも注目してみると良いかもしれません。
勉強になりました。

おわりに

まとめるとこんな感じです。

純Python Python+Cython 最適化 C言語ライブラリ Python+Cython+並列処理
Time 1min56s 1min14s 9.96s 6.77s 3.64s
向上率 0% 43% 85% 90% 97%

割と速さを実感できる結果になりました。

このようにjupyterで簡単にcythonを使えることが伝わったと思います。

この最適化に関しては、自分なりに試行錯誤した結果なのでこれが必ずしも正しいというわけではないのであしからず。

高速化の手助けに少しでもなればと思います。