静的型付けでコードを高速化する

Cython は Python コードコンパイラです。つまり、通常の Python コードを、 特に手を加えずにコンパイルできます (もちろん、いくつか未対応の言語機能 を例外として、ですが)。しかし、決定的にパフォーマンスを必要とするコー ドでは、静的な型宣言をしておくと役に立つことがあります。というのも、 Cython がそうしたコードを扱うときに、 Python コードの動的な取り扱いか ら一歩踏み出して、より単純で高速な C のコードを生成するからです。 生成されたコードは、時として相当なオーダで高速になります。

とはいえ、型宣言はコードを冗長で読みにくくするということも忘れてはなり ません。ですから、「ベンチマークによって、パフォーマンス・クリティカル な部分が特定された」というような適切な理由がない限り、型宣言を使うのは 止めましょう。大抵の場合、適切な場所にちょっと型定義を入れるだけで、大 幅に性能向上できます。

Cython の型宣言は、整数、浮動小数点型、複素数、構造体、共用体、ポイン タ型といった、あらゆる C のデータ型を網羅しています。 Cython は、デー タへの代入の際に、自動的かつ適切に値の変換を行います。また、例えば Python の長整数型を扱う際に、 C の型変換でオーバフローが発生すれば、 実行時に OverflowError を送出させる機能も備えています (ただし、算 術演算中のオーバフローは検出していません)。Cython が生成する C のコー ドは、プラットフォーム固有の C データ型サイズを正しく安全に扱います。

型の定義には、予約語 cdef を使います。

変数の型を宣言する

以下のような、 pure Python のコードがあったとします:

def f(x):
    return x**2-x

def integrate_f(a, b, N):
    s = 0
    dx = (b-a)/N
    for i in range(N):
        s += f(a+i*dx)
    return s * dx

このコードを単に Cython でコンパイルすると、だいたい 35% くらい高速化 します。これでも、何もしないよりは十分ましですが、静的な型定義をすると、 違いはもっと際立ちます。

型宣言をつけると、コードは以下のようになります:

def f(double x):
    return x**2-x

def integrate_f(double a, double b, int N):
    cdef int i
    cdef double s, dx
    s = 0
    dx = (b-a)/N
    for i in range(N):
        s += f(a+i*dx)
    return s * dx

このコードを Cython で処理すると、イテレータ変数 i が C のセマンティ クスで型定義されているので、 for ループが完全に C のコードにコンパイル されます。 a, s, dx は、 for ループの中の算術演算に使われ ているので、型宣言しておく必要があります。 bN を型定義して もあまり差異はないのですが、ここでは大した作業ではないので、一貫性を持 たせたり、関数全体の型定義をはっきりさせるために定義しています。

このコードを実行すると、スピードは pure Python の 4 倍に上がります。

関数の型定義

Python の関数呼び出しというのは、高いコストを要する操作です – そして、 Cython の中では、関数呼び出しの際に Python オブジェクトの変換が双方向 で行われるため、その重みは更に倍加します。 上の例では、 f() の中でも f() を呼び出しているコード中でも引数に C の double 型を想定しているにもかかわらず、戻り値を受け渡しするときに、い ちいち Python float オブジェクトを生成せねばなりません。

Cython では、 cdef キーワードを使った構文で、C 形式の関数を宣言できる ようにしています:

cdef double f(double x) except? -2:
    return x**2-x

通常は、何らかの except 修飾子を追加します。そうしなければ、Cython は 関数内 (や、関数を呼び出しているコード) で送出された例外をうまく伝播で きないからです。 except? -2 は、関数が -2 を返さないかエラー チェックを行います (ただし、 ? を使うと、エラー処理の結果、 -2 は有効な戻り値として呼び出し側に返されることがあります)。 except * を使うと、実行速度は遅くなりますが、より安全です。 関数が Python オブジェクトを返すときや、関数呼び出し中で例外が送出され ないことが保証されている場合には、 except 節の内容は無視されます。

cdef には、 cdef を使って定義した関数が Python 側から見えなくなるので、 呼び出す方法がなくなるという副作用もあります。 cpdef キーワードを 使って定義すれば、 Python オブジェクトのラッパも同時に生成されるので、 関数を Cython 側 (高速、かつ値を直接渡す) と Python 側 (値を Python オ ブジェクトでラップする) の両方から呼び出せます。

それから、 f を実行時に変更することもできなくなるので注意しましょ う。

このコードは、 pure Python の 150 倍高速化します。

どこに型定義を追加するか決めるには

静的な型定義が大幅な高速化の鍵なので、初心者は目につくものをなんでも型 付けしがちです。しかし、その結果、コードからは可読性と柔軟性が失われて いきます。逆に、クリティカルなループ変数の型定義をちょっと忘れただけで、 パフォーマンスは簡単に失われてしまいます。この微妙な型定義の作業に役立 つのが、プロファイリングとアノテーションという二つのツールです。 プロファイリングは、最適化を行う上での最初のステップで、コードのどこが 処理時間を食いつぶしているかを教えてくれます。次に、 Cython のアノテー ション機能が、なぜコードの実行に時間がかかっているかを教えてくれるはず です。

cython コマンドラインプログラムに -a オプションを付けて実行す る (または、Sage のノートブックからリンクをたどる) と、 Cython のコー ドと生成された C のコードが交互に記述された HTML が出力されます。各行 は、「型付け度 (typedness)」によって色分けされています – 白色で示され た行は、 Python API の呼び出しを全く含まない素の C のコードに変換され ています。このレポートは、関数の実行速度を最適化する上で、とても貴重な 材料となるでしょう。

../../_images/htmlreport.png