Cython コードを PyPy に移植する

バージョン 0.17 から、 Cython は cpyext の基本的な機能をサポートしてい ます。 cpyext は CPython の C/API をエミュレートする PyPy のレイヤです。この機能は、 Cython が生成す る C のコードを、C のコンパイル時に適応させる方法で実現しているので、 コードは CPython と PyPy の両方で変更の必要なくコンパイルできます。

ただし、 Cython が内部的にカバーしたり適応したりする部分の他にも、 cpyext C/API エミュレーションには、CPython における実際の C/API との違 いがあり、ユーザコードに目に見える影響があります。この節では、CPython と PyPy の両方で動く Cython のコードを書くために取り扱わねばならない、 主な違いを挙げています。

参照カウント

PyPy の設計上の大きな違いは、ランタイムシステムが内部的に参照カウント を使わず、常にガベージコレクタを使うという点です。参照カウントは cpyext のレイヤで実現されており、 C 側での参照カウントだけを保持してい ます。そのため、 PyPy の参照カウントは、 Python 側の参照を一切カウン トしていないという点で、CPython の参照カウントと大きく異なります。

オブジェクトの寿命

ガベージコレクションの性質が異なるため、オブジェクトは CPython とは違っ た形で寿命を終えます。そのため、 CPython ではすでに死んでいるはずのオ ブジェクトが、 PyPy ではまだ生き残っているかもしれず、このことに注意し てやる必要があります。とりわけ、拡張型のデアロケータメソッド (__dealloc__()) が呼ばれるのは、CPython に比べてずっと後であり、オ ブジェクトの死ではなく、メモリ不足によって引き起こされます。

あるオブジェクトが (例えば他のオブジェクトや、関数の実行時に関連して)、 コード中のどこで死すべきかが分かっていれば、デアロケータの動作に依存す るより、その時点でオブジェクトを無効化して、手動で消去できないか考えた ほうがよいでしょう。

そうした書き方の副作用として、コンテキストマネジャを with 文と一緒 に使うといった、よりベターなコード設計にたどり着くこともあります。

借用参照 (borrowed reference) とデータポインタ

PyPy のメモリ管理機構は、メモリ上のオブジェクトの移動を認めています。 PyPy の C/API レイヤは、PyPy オブジェクトの間接的なビューでしかなく、 しばしばオブジェクトのデータやステートを C 空間に複製して、もとの PyPy オブジェクトではなく、 C/API オブジェクトの寿命に結びつけてしまいます。 肝心なのは、これら二つのオブジェクトは、 cpyext 上は別々のものだという ことです。

その影響で、例えば、データポインタや借用参照を使うと、C 空間からはそれ を所有しているオブジェクトを直接参照できず、参照やデータポインタは、オ ブジェクトがまだ生きているにも関わらずある時点で無効になるかもしれない、 といったことが起こりえます。 CPython と違って、生きているオブジェクト への参照をリスト (や、Python のコンテナ) に入れておくだけでは充分では ありません。コンテナの中身を管理しているのは Python 側だけで、コンテナ が参照しているのは PyPy オブジェクトだけだからです。そして、 Python コ ンテナ中にオブジェクトへの参照を持っていても、オブジェクトに対する C-API ビューを生かしつづけることはできません。 Python のクラス辞書中の エントリも、同様に使えないことは明らかです。

こうした状況がよく起きるのは、バイト文字列の char* バッファ にアクセスするときでしょう。 PyPy でうまく動作するのは、 Cython のコー ドがバイト文字列オブジェクト自体への直接参照を維持している場合に限りま す。

もう一つのポイントは、例えば PyTuple_GET_ITEM() などのような CPython の C/API 関数を直接呼び出して、借用参照を返させるときです。 関数によっては、組み込みモジュールや実行環境中の低水準のオブジェクトへ の借用参照を返します。 このとき PyPy の GIL によって借用参照の有効性が保証されるのは、次の PyPy の (またはPyPy の C/API の) 呼び出しまでで、それ以降は有効とは限 りません。

従って、次の PyPy の呼び出し以後に Python オブジェクトの内部アクセスし たり、借用参照を使ったりする時には、参照カウントの操作や GIL を開放す る操作も含めて、 C のコード側、例えば関数のローカル変数や、拡張型のア トリビュートを使って、オブジェクトへの所有参照を別に維持しなければなり ません。

借用参照の寿命が不明なときは、借用参照を返す C/API の利用を避けるか、 借用参照を使う前後に Py_INCREF()Py_DECREF() を 入れて、明に所有参照に変えて下さい。

組み込み型、スロット、フィールド

現状、 PyComplexObject, PyFloatObject, PyBoolObject は、 cpyext の中では C レベルの表現で使えません。

cpyext は組み込み型のスロット関数のほとんどを初期化しないので、直接使 えません。

同様に、組み込み方の構造体の (実装固有の) フィールドはほとんどが C レ ベルから見えません。例えば PyLongObjectob_digit フィー ルドや、 PyListObjectallocated フィールドなどにはア クセスできません。コンテナの ob_size フィールド (Py_SIZE() マクロで使っています) は読めますが、正しい値が入っ ているとは保証できません。

これら構造体のフィールドやスロットにはアクセスせず、通常の Python の型 情報を使い、通常の Python のプロトコルでオブジェクトを操作するのがベス トです。 Cython は CPython と cpyext の両方で C/API の適切な呼び出し操 作に対応付けます。

効率

CPython では速度向上のために簡単な関数やマクロを使っていますが、これら は cpyext では明らかに異なるパフォーマンス特性を示します。

借用参照を返す関数が特別なケアを必要とすることはすでに触れましたが、そ の他にも、かなりの実行時オーバヘッドを発生させるという特徴があります。 これらの関数は、 CPython では単に素のポインタを返すだけなのに対して、 PyPy 中ではしばしば弱参照を生成するからです。 PyTuple_GET_ITEM() がその例です。

より高水準の関数の中には、全く違うパフォーマンス特性を示すものもありま す。例えば、辞書のイテレーションに使う PyDict_Next() です。 この関数は CPython では辞書のイテレーションを高速実行するのに使い、 計算複雑度は線形でオーバヘッドも低いのですが、現状、 PyPy での計算複雑 度は2乗です。というのも、PyPy ではこの関数は通常の辞書のイテレーション に対応づけられていて、呼び出しの間でイテレーションの現在位置を追跡でき ないため、毎度イテレーションを実行せねばならないからです。

一般的なアドバイスは、これは CPython に対してもいえることですが、 – 何をしているのか理解しているのでないかぎり – C-API を直接使うより も、Cython が C/API を操作するコードを適切に生成するのに任せるのがベス トだということです。 もし、 PyPy や cpyext で何かする際に、 Cython の現状のやり方よりもいい 方法を思いついたら、皆さんの利益のためにぜひ Cython を修正してください。

既知の問題

  • PyPy 1.9 の時点では、組み込み型のサブタイプを定義すると、ごく稀にメ ソッドの無限再帰呼び出しを引き起こします。
  • 特殊メソッドの docstring は Python 側に伝わりません。

バグとクラッシュ

C/API とその下にある CPython のネイティブ実装がよくテストされているの に対して、PyPy の cpyext 実装はまだ若く、かなり未熟です。そのため、プ ログラムがクラッシュしたときに、原因が常に Cython で書いたあなたのコー ドにあるとは限りません。また、 PyPy と cpyext 実装は、C レベルのデバッ グを意識した設計でないため、CPython や Cython に比べ、C レベルでのデバッ グが困難です。