PyPy(cpyext)におけるC APIやCオブジェクトモデルのエミュレート方法
![Adventures in compatibility: emulating CPython's C API in PyPy](http://cz-cdn.shoeisha.jp/static/images/article/11097/11097_006.jpg)
- タイトル:Adventures in compatibility: emulating CPython's C API in PyPy
- 発表者:Ronan Lamy
- セッション動画
PyPyはRPythonツールチェインを使って実装されたPython処理系です。Pythonの仕様に準拠していながら、CPythonよりも高速に動作するとされています。PyPyにおいてC言語で書かれたプログラムを呼び出す際には、CFFI
が最も簡潔で効率的な方法ですが、NumPyのようにCPythonのC APIを利用しているC拡張モジュールとして多くの優れたライブラリが存在します。
以前はCPythonを別プロセスで起動して、TCP経由でRPCコマンドをやりとりする必要がありました[3]。しかし現在はcpyext
というモジュールがC APIをエミュレートすることで、CPythonを起動することなくC拡張モジュールを実行できます。これをどのように実現したのかについて、PyPyのコア開発者によって解説されました。
PyPyのアーキテクチャ
本題に入る前にPyPyがCPythonよりも速いとされている理由について解説がありました。PyPyのパフォーマンスが優れている理由はいくつかありますが、最も寄与しているものはJITコンパイラによるものです。PyPyのアーキテクチャをみてみましょう。
![PyPyのアーキテクチャ](http://cz-cdn.shoeisha.jp/static/images/article/11097/11097_007.png)
CPythonでは生成したバイトコードをただバイトコードインタープリターが処理しますが、PyPyではバイトコードインタプリターが処理をしながらランタイム情報をトレースします。具体的にはソースコードのどの部分が何回実行されたのかといった情報や、実行時の型情報を解析しています[4]。そこで得たランタイム情報をもとに最適化されたマシンコードを生成しています。JITのコンパイラは実装が難しく複雑になりますが、RPythonツールチェインの中にはJITコンパイラの実装をサポートするためのトレースオプティマイザーが組み込まれています。詳細はRPythonのドキュメントを参照してください。
cpyextによるCPythonのC APIのエミュレート
PyPyにはC APIをエミュレートするためのモジュールとしてcpyext
が存在します。その中身を見ると次のようになっています。
# 参照: https://bitbucket.org/pypy/pypy/src/45c392a241f5d7d11a925beeb3e7e3b3bb342a03/pypy/module/cpyext/dictobject.py @cpython_api([PyObject], Py_ssize_t, error=-1) def PyDict_Size(space, w_obj): return space.len_w(w_obj)
このコードはC APIの処理がPythonで再実装されていることを示しています。cpython_api
デコレーターによりC APIとPythonの関数が紐付けられます。これはどのようにC拡張から呼び出されるのでしょうか?
C拡張モジュールは、呼び出したいCの関数をPython.h(Cヘッダーファイル)の情報をもとに特定します。PyPyでは独自のPython.hを内部で用意していて、C拡張はそのヘッダーファイルをもとにコンパイルされます。実行時にはPyPyが提供しているCのインターフェイスを経由してPyPyインタープリター上にもオブジェクトが生成されます。そしてC API呼び出しにフックして先程の例に示したPythonで記述された関数が呼び出されます。
そうはいっても、もともとC言語で実装されたAPIを、RPythonで再実装するのは当然簡単なことではありません。両者は全然違う言語ですので、多くの違いがあります[4]。文字数の都合上すべてをここで解説することはできませんが、ガベージコレクション(以下、GC)のアルゴリズムの違いについてここでは触れようかと思います。
CPythonはご存知の通り参照カウントを計算していて、あるオブジェクトの参照カウントが0になったタイミングでメモリからすぐに開放されます[5]。しかしPyPyでは参照カウントを管理せずincminimark[6]とよばれるGCが走るタイミングに依存してオブジェクトがメモリから開放されます。
そこでPyPyではC拡張が扱うCオブジェクトと、PyPyインタープリター上に存在するオブジェクトをセットで管理し、参照カウントが0もしくはPyPyのオブジェクトが到達不能になりGCによって破棄されたタイミングで同時に破棄しているとのことでした。
余談:Victor StinnerによるC APIの改善提案
少し余談になりますが、このセッションについて筆者(芝田)がTwitterでつぶやいた時、CPythonのコア開発者であるVictor Stinnerからリプライがありました。
The C API is wrong. Let's fix it! https://t.co/NBkSVxDfbR
— Victor Stinner
どうやらCPythonのC APIに課題があるらしく、改善しようと動いているようです。
当然既存のC拡張が動作するように後方互換を保ったまま、修正する必要があるのですが、詳細は彼の公開したWebサイトにまとまっているようです。かなり長いドキュメントなので筆者はまだ読んでいないのですが、もし興味を持った方はぜひ読んでみてください。
注
[3] PyPyのブログ記事に記載がありました(「Using CPython extension modules with PyPy, or: PyQt on PyPy」を参照)。
[4] PyPyのドキュメントにも、CPythonとPyPyの違いについてまとまったページがありました(「Differences between PyPy and CPython」を参照)。
[5] 循環参照などにより参照カウントが0にならず、破棄されずに残ってしまったオブジェクトは周期的に実行されるマークアンドスイープのようなGCによって破棄されます。ここで「のような」とぼかしたのは理由があるのですが、石元敦夫氏による詳しい解説があるためそちらをご覧ください。
[6] PyPyはもともとMinimark GCとよばれるGCを実装し、利用していました。その後LuaJITで利用されているIncremental GCのアイデアを取り入れました。これを incminimark(Incremental version of Minimark GC)と呼んでいます。詳細はPyPyのブログ記事を参照してください。