深入りしないCython入門

  • 62
    いいね
  • 0
    コメント

Cythonとは?

Pythonは処理速度は決して早くない、むしろ遅い部類である。
そこで、C/C++に変換することにより高速化しようというのがCythonである。

低級言語のC/C++(昔は高級言語だったが、現在は低級言語といって良いだろう)に変換してネイティブコンパイルするのだから、速いに決まっている。

この記事の目的

「Cythonは難しい」「CythonはC/C++の知識が必要」という印象があるだろう。答えは「Yes」である。

しかし、その答えはCythonをフルに使いこなす事が前提である。
実はCythonは、C/C++をそんなに知らなくても、ちょっとした高速化には充分な恩恵を得られるように設計されているのだ。

しかし、無闇にCythonを使っても「あまり高速化されない」、「移行がすごく大変」と散々な結果になるだろう。そこら辺のポイントを踏まえ、C/C++をあまり知らない初心者にもCythonのおいしい所をつまみ食いできるようになることが、本記事の目的である。

この記事で説明しないこと

なお、この記事ではCythonのインストール方法や、setup.pyなどの書き方は説明しない。Googleで調べればすぐに出て来るので検索してほしい。

WindowsでもCythonを使えるようにする。

この記事では、Cythonの深部には触れず、美味しいところをつまみ食いするためのもので、Cythonの深い部分は説明しない。非常にありがたい事に、Cythonのドキュメントを和訳されている、Cythonの深い部分を知りたい場合は、下の記事を参照されたい。

Cython ドキュメント(和訳)

Cythonで高速化しない方がよいケース

処理速度が速くなるからといって、やたらめったら高速化すべきではない、むしろCythonで高速化で効果が出る箇所の方が少ないと言ってもよい。

CPUバウンド以外を高速化すること

プログラムの実行速度というのは単純な処理速度だけで決まるものではない。
処理速度の向上で改善できるのは、演算処理が多い事による待ち(CPUバウンド)だけであり、メモリアクセスの待ち(メモリバウンド)や、HDDなどからのデータ読み込み待ち(I/Oバウンド)などには殆ど効果がない。
それらはの問題は、アルゴリズムの改良(メモ化など)や、スレッド/マルチプロセス化で対処すべきである。

プログラムを全て高速化すること

Cythonによる高速化はC/C++への移植に比べて簡単ではあるが、やはり負担が掛かる。
苦労してプログラム全体を高速化しても、あまり意味がない事が多い。
1回しか呼ばれない関数を10%高速化するより、1000回呼ばれる関数を1%高速化したほうが、はるかに効率が良いのである。

つまり高速化すべき場所は、全体のごく一部しかない

上記の2点は高速化の大原則であり常識である。
重要なことは、プログラム全体を高速化するのではなく、ボトルネックとなる原因と場所を理解し、適切な対応をするという事だ。
逆説的ではあるが、言語の処理速度はさほど重要ではない、アルゴリズムの改善の方がよほど重要で、実行速度の問題はアルゴリズムの改善で解消できる事がほとんどである。

ボトルネックの原因によっては、SWIGやNumbaの方が適している場合があるので、適材適所で使い分ける。

  • SWIG
    • 既存のC/C++ライブラリをPythonから呼び出すためのツール
  • Numba
    • Jitコンパイラにより、ボトルネックを計測して最適化する、ボトルネックが広範囲、または動的に変化する場合に有効

Cythonで改善するコード

ループ処理

Pythonのループ処理は遅い。
配列に様々な型が格納できるという自由度ゆえに、ループ時に様々なチェックが入るためである。
高速なNumpyを使用していても配列をforで回せば劇的に速度は低下する、Numpyを使用する際はループ処理を避けるのが鉄則である。

演算処理

動的型付け言語ゆえの自由度のため、演算ごとに変数の型チェックが入ってしまう。
たとえa + bという単純な演算でも、__add__メソッドを経由しなければならないため、演算ごとにオーバーヘッドが発生してしまう。

関数呼び出し

Pythonに限らないことだが、関数呼び出しは大きなオーバーヘッドが発生する。
特に動的型付け言語は、スタックへの代入時にも型チェックが入るため、静的型付け言語よりもオーバーヘッドは大きい。

Jupyter(IPython)で始めるCython

Cythonを実装はJupyterが最適である。CythonはJupyterのmagick commandに対応しており、Jupyter上で気軽に試すことができる。

Jupyter上で、以下のコマンドを打ちCythonを有効にする。

%load_ext Cython

みんな大好きフィボナッチ数の実験をしてみよう。
まずはPythonで、フィボナッチ数を求めるPython関数を作る。

def py_fib(n):
    a, b = 0.0, 1.0
    for i in range(n):
        a, b = a + b, a
    return a
速度計測
%timeit py_fib(1000)
>>> 10000 loops, best of 3: 66.9 µs per loop

次にマジックコマンド%%cythonと先頭に打ち、先程のPython関数と内容が全く同じ関数を作ってみよう。繰り返していうが、関数名が違うだけで内容はPythonのものと全く同じである。

%%cython
def cy_fib(n):
    a, b = 0.0, 1.0
    for i in range(n):
        a, b = a + b, a
    return a
速度計測
%timeit cy_fib(1000)
>>> 100000 loops, best of 3: 16 µs per loop

Python関数をCythonにしただけで66.9µs → 16µsになった、何もしていないのに4倍以上の高速化である。
Cythonは、Pythonの構文との互換性がかなり高く、あまりに特殊なPythonコードでなければ、CythonがそのままC/C++に変換してくれるのだ。

更に高速化するために、型指定をしてみよう。

%%cython
def cy_fib2(int n):
    a, b = 0.0, 1.0
    for i in range(n):
        a, b = a + b, a
    return a
速度計測
%timeit cy_fib2(1000)
>>> 1000000 loops, best of 3: 1.11 µs per loop

66.9µs → 1.11µsになり60倍の高速化になった。

ここで重要なことは、def cy_fin2(int n)の部分しか型定義していないという事だ、そこ以外はPython関数と全く一緒である。

実はCythonが型推論をして、変数abはdouble型に、変数iはint型に自動変換しているのだ。Pythonコードをそのまま貼り付けても、なるべくPythonの元コードと同じ動作をして、かつパフォーマンスを損なわないようにC/C++に変換するようになっている。

Cythonでは、やみくもな型指定は可読性を下げるだけであまり意味がないのだ、いくつかの変数を型定義するだけで、数十倍の高速化が達成されるケースがほとんどだ。

なお、%%cython -aと打って実行すると、PythonとC/C++併記と、処理に時間が掛かっている行を黄色で表示してくれる。

cython.png

まずは、何も考えずPythonコードをコピペして、黄色い行を優先的に型指定していくのが、正当なCythonの使い方だろう。

まとめ

まとめると、高速化において最適化が必要な部分はプログラムコードのごく一部の関数であり、さらにその関数の中でもCythonで最適化すべき行はごく僅かである。
なので可読性を保ったまま、かつ楽ちんに達成しようというのがCythonである。

Cythonは難しいと思われがちだが、Cython開発者の努力によりほぼ完璧なPythonとの互換性を備えている。部分的に高速化するのであれば、C/C++を知らなくても充分な恩恵を得ることができるのだ。
(99%のケースではいくつかの関数を高速化するだけで、目的は達成されるだろう)

Cythonは非常に奥が深い、さらなる使い方の解説はまたの機会に。

深入りしないCython入門 -2-に続く。