原文はこちら。
The original article was written by Mohaned Qunaibit (Senior Researcher at Oracle).
https://medium.com/graalvm/porting-matplotlib-from-c-api-to-hpy-aa32faa1f0b5
GraalVMは、Python、JavaScript、Rubyを含む複数の言語をサポートする真のpolyglot runtimeです。GraalVMチームの仕事は、言語の実装とサポートにとどまらないことが多く、ランタイム上で実行される言語のエコシステムへの積極的な貢献もしています。その一環として、Pythonのエコシステムで、既存のCPython C APIの代替となるHPyプロジェクトにコントリビュートしています。
Python/C API Reference Manual
https://docs.python.org/3/c-api/index.html
CPython C APIは、Python言語周辺の広範なエコシステムに大きく貢献しました。CPythonの言語実装と密接に結びついたC APIとの直接的なやりとりは、Pythonの多くの取り組みの妨げになってきました。C APIを使ったモジュール拡張は、CPythonの実装と密接に結合してしまい、CPythonの進化を難しくしています。
Pythonのバージョンや、異なるPythonの実装間でもバイナリの互換性を提供するものをCPython C APIの代替として提案しており、それがHPyです。これにより、CPythonの内部に直接依存せずに、PythonのリリースからC拡張のバイナリが切り離されます。したがって、CPythonの内部が変更されても、あなたのバイナリが壊れることはないはずです。
Port to HPy assurance
HPyでは、C APIとHPyの両方を並行して動作させるインクリメンタル・ポーティング・プロセスが可能です。
HPy overview
https://docs.hpyproject.org/en/latest/overview.html
次の例は、C API と HPy の両方を持っている様子を示しています。
#include <Python.h> | |
static PyObject *classA_foo(PyObject *self, PyObject *arg) | |
{ | |
Py_INCREF(arg); | |
return arg; | |
} | |
HPyDef_METH(classA_bar, "bar", classA_bar_impl, HPyFunc_NOARGS) | |
static HPy classA_bar_impl(HPyContext *ctx, HPy self) | |
{ | |
return HPyLong_FromLong(ctx, 1234); | |
} | |
static PyMethodDef classA_methods[] = { | |
{"foo", classA_foo, METH_O}, | |
{NULL, NULL} /* Sentinel */ | |
}; | |
static PyType_Slot classA_type_slots[] = { | |
{Py_tp_methods, classA_methods}, | |
{0, 0}, | |
}; | |
static HPyDef *classA_type_defines[] = { | |
&classA_bar, | |
NULL | |
}; | |
static HPyType_Spec classA_type_spec = { | |
.name = "ClassA", | |
.legacy = true, | |
.legacy_slots = classA_type_slots, | |
.defines = classA_type_defines | |
}; |
Example of a type implemented using both C API and HPy
この例でわかるように、foo
はCのAPIで、bar
はHPyで実装されており、どちらのメソッドもClassA
型に対してアクセス可能です。この機能は、一度に全面的な書き換えを必要とせず、ひとつひとつHPyへ移植作業ができるようになります。さらに、HPyにはデバッグモードという、メモリリークやuse-after-free(領域開放後の利用)などを検出できるキラー機能があります。詳しくは以下をご覧ください。
Debug Mode
https://docs.hpyproject.org/en/latest/debug-mode.html#debug-mode
Why Matplotlib?
Matplotlibは最も人気のあるPythonパッケージの1つで、特にデータサイエンス用途に適しています。
Matplotlib
https://matplotlib.org
Matplotlibは、使いやすいAPIでグラフやアニメーションを作成する、包括的なPythonツールです。Matplotlibの重い計算のほとんどはC/C++で実装されており、部分的にはC APIを呼び出しています。さらに、計算の一部を高速化するために、NumPyのネイティブAPIを使用しています。このように、MatplotlibのHPyへの移植は、性能、互換性、複雑性の面で、いくつかのユニークな課題があります。
Porting Matplotlib C API to HPy
HPyへのMatplotlibの移植にあたり、C APIを使用して実装された10のモジュールがあります。
backend_agg
_c_internal_utils
_contour
_image
_path
_tkagg
_ttconv
ft2font
_qhull
_tri
これらの Matplotlib モジュール上に、2つの C 拡張モジュールの依存性があります。
- NumPy
https://github.com/numpy/numpy - Kiwisolver
https://github.com/nucleic/kiwi
NumPyはそれ自体が非常に大きいので、HPyによる可能になっている漸進的な移植を利用し、Matplotlib のNumPy APIを使用する部分のC APIを維持しようと考えています。一方Kiwisolverは、完全にHPyに移植しました。
移植作業の大部分はかなり単純です。HPyの命名規則により、既存のPython/C APIに相当するHPyが何であるかは一目瞭然です。以下にいくつかの例を挙げます。
// src/_c_internal_utils.c
+ static HPy
- static PyObject*
mpl_SetCurrentProcessExplicitAppUserModelID(
+ HPyContext *ctx,
+ HPy module,
- PyObject* module,
+ HPy arg
- PyObject* arg
) {
#ifdef _WIN32
+ wchar_t* appid = HPyUnicode_AsWideCharString(ctx, arg, NULL);
- wchar_t* appid = PyUnicode_AsWideCharString(arg, NULL);
...
}
// src/qhull_wrap.c
+ static HPy
- static PyObject*
delaunay(
+ HPyContext *ctx,
+ HPy h_self,
- PyObject *self,
+ HPy* args,
+ HPy_ssize_t nargs
- PyObject *args
) {
HPy xarg;
HPy yarg;
...
+ if (!HPyArg_Parse(ctx, NULL, args, nargs, "OO", &xarg, &yarg)) {
- if (!PyArg_ParseTuple(args, "OO", &xarg, &yarg)) {
+ HPyErr_SetString(ctx, ctx->h_ValueError, "expecting x and y arrays");
- PyErr_SetString(PyExc_ValueError, "expecting x and y arrays");
+ return HPy_NULL;
- return NULL;
}
...
}
Snippet of the Matplotlib HPy port
例に示すように、相違点は単に古いルーチンやターゲット関数の名前に影響を与えるだけにすぎません。変更点をまとめると以下のようです。
- C API関数の前に
H
を追加 - 第1引数として
HPyContext *
型のctx
を持つ - タプル
arg
を配列形式の* args
と配列長nargs
で置換 PyObject *
をHPy
で置換
The HPyContext *
引数はPythonからCへのダウンコールのたびにHPyが自動的に提供します。HPyContext は全ハンドルが属するPythonインタプリタを表します。これにはPythonの標準グローバル変数が含まれます。
HPy API introduction
https://docs.hpyproject.org/en/latest/api.html#hpycontext
したがって、標準例外PyExc_ValueError
はctx->h_ValueError
に置き換わります。
Arguments Parser
HPyの引数は、タプルではなく配列として、callee (呼び出される側)に渡されます。これにより、HPyArg_Parse
とHPyArg_ParseKeywords
のパース処理が容易になります。
Argument Parsing
https://docs.hpyproject.org/en/latest/api-reference/argument-parsing.html#argument-parsing
HPyの引数の解析は、C APIのPyArg_ParseTuple
の処理と比較すると、一般的なユースケースでより簡素化されたルーチンで独自の内部ツールを使用して行われます。
hpy/argparse.c
https://github.com/hpyproject/hpy/blob/master/hpy/devel/src/runtime/argparse.c#L2
cpython/getargs.c
https://github.com/python/cpython/blob/main/Python/getargs.c
HPyの引数パーサーのシンプルさの結果、手動で実装する必要があるいくつかの重要でない機能を欠くという代償を伴います。以下はその例です。
// src/tri/_tri_wrapper.cpp
static HPy PyTriangulation_calculate_plane_coefficients(
HPyContext *ctx, HPy h_self, HPy* args, HPy_ssize_t nargs, HPy kwds)
{
Triangulation::CoordinateArray z;
+ PyTriangulation* self = PyTriangulation_AsStruct(ctx, h_self);
+ HPy h_z;
+ if (!HPyArg_Parse(ctx, NULL, args, nargs, "O:calculate_plane_coefficients", &h_z)) {
+ return HPy_NULL;
+ }
+ if (!z.converter(HPy_AsPyObject(ctx, h_z), &z)) {
...
+ return HPy_NULL;
+ }
- if (!PyArg_ParseTuple(args, "O&:calculate_plane_coefficients",
- &z.converter, &z)) {
- return NULL;
- }
...
}
C API の PyArg_ParseTuple
関数と&
コマンドを使用すると、関数ポインタとして渡される変換関数の助けによって Python オブジェクトを変換します。
Parsing arguments and building values
https://docs.python.org/3/c-api/arg.html#other-objects
この機能はHPyでは利用できないので、この場合はHPyArg_Parse
コマンドと&
を削除し、上の例のz.converter
と同様に引数をパースした後に手動で変換を行う必要があります。
From Pointers to Handles
HPy
オブジェクトはハンドルを表します。それゆえ、HPyのHはPythonオブジェクトを指しています。
HPy API introduction
https://docs.hpyproject.org/en/latest/api.html#handles
PyObject *
を直接使わずにポインタをハンドルの中にカプセル化するのには利点があります。
- Pythonオブジェクトの実装の詳細をホストのPythonインタープリタから抽象化できるため、例えば、moving GCのような異なるGC実装を持つことができる
Garbage collection (computer science)
https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)
Tracing garbage collection
https://en.wikipedia.org/wiki/Tracing_garbage_collection
- メモリ解析ツールがリークしたハンドルや閉じた後に使用されたハンドルのケースを検出するにあたり、より多くの可視性を提供する。
Debug Mode
https://docs.hpyproject.org/en/latest/debug-mode.html#debug-mode
HPyのアプローチは、Pythonオブジェクトの割り当てと解放について顕著な違いがあります。関数から返されるハンドルは決して借用されず、関数の引数として渡されるハンドルは決して盗まれることはありません。つまり、HPy API関数の呼び出しからハンドルを取得した場合、HPy_Close
を使ってハンドルを閉じる責任があります。一方、引数として渡されたハンドルは、決して閉じてはいけません。代わりに、ハンドルを返す必要がある場合は、ハンドルのコピー、つまりHPy_Dup
を作成する必要があります。HPy
オブジェクトを扱うときに注意しなければならないことがあります。
HPyは短命のハンドルで、PythonからHPy拡張関数が1回呼び出される間しか生存できません。
Porting guide
https://docs.hpyproject.org/en/latest/porting-guide.html
もう一つの顕著な違いは、複数のハンドルが同じPythonオブジェクトを指している場合でも、それぞれのハンドルは独立して動作することです。さらに、ハンドル同士を直接比較することはできませんが、代わりにHPy_Is
を使用するか、ハンドルがNULL
を指しているかどうかをチェックするためにHPy_IsNull
を使用できます。詳細は以下を参照してください。
HPy API introduction
https://docs.hpyproject.org/en/latest/api.html#handles-vs-pyobject
Handling Type Fields
C API を使って実装されたいくつかの Python型は、高速アクセスのため、および/または Python 以外のオブジェクトを格納するために、構造体の中にフィールドの値を格納します。これらの型はデータ構造へのポインタ、すなわちPyObject_HEAD
メンバマクロを使用してPyObject
とレイアウトを共有するstruct
(構造体)を持つことに依存しています。一方、HPyでは、型の構造体をPythonオブジェクトから分離し、それにアクセスするためのAPIを提供します。このように分離することで、ホストPythonインタプリタのガベージコレクタ(GC)が副作用に遭遇することなくデータを自由に移動できます。しかし、型オブジェクトがPyObject*
を参照している場合、HPy
の寿命は短く、参照されてはならないので、置き換えはHPy
ではなく、HPyField
でなければなりません。以下に、この動作の例を示します。
// src/ft2font_wrapper.cpp
typedef struct
{
- PyObject_HEAD
FT2Font *x;
+ HPyField fname;
- PyObject *fname;
+ HPyField py_file;
- PyObject *py_file;
FT_StreamRec stream;
HPy_ssize_t shape[2];
HPy_ssize_t strides[2];
HPy_ssize_t suboffsets[2];
} PyFT2Font;
+ HPyType_HELPERS(PyFT2Font)
static HPy PyFT2Font_new(HPyContext *ctx, HPy type, HPy* args, HPy_ssize_t nargs, HPy kwds)
{
PyFT2Font *self;
+ HPy h_self = HPy_New(ctx, type, &self);
- self = (PyFT2Font *)type->tp_alloc(type, 0);
self->x = NULL;
+ HPyField_Store(ctx, h_self, &self->fname, HPy_NULL);
- self->fname = NULL;
+ HPyField_Store(ctx, h_self, &self->py_file, HPy_NULL);
- self->py_file = NULL;
memset(&self->stream, 0, sizeof(FT_StreamRec));
return h_self;
}
...
static HPy PyFT2Font_get_num_glyphs(HPyContext *ctx, HPy h_self)
{
+ PyFT2Font* self = PyFT2Font_AsStruct(ctx, h_self);
+ return HPyLong_FromLong(ctx, self->x->get_num_glyphs());
- return PyLong_FromLong(self->x->get_num_glyphs());
}
...
static HPy PyFT2Font_fname(HPyContext *ctx, HPy h_self, void *closure)
{
+ PyFT2Font* self = PyFT2Font_AsStruct(ctx, h_self);
+ HPy fname = HPyField_Load(ctx, h_self, self->fname);
+ if (!HPy_IsNull(fname)) {
- if (self->fname) {
+ return fname;
- return self->fname;
}
return HPy_Dup(ctx, ctx->h_None);
}
上記の例では、PyFT2Font
型には複数のフィールドがあり、いくつかフィールドはPythonオブジェクト、いくつかはそうではないものです。During object instantiation, i.e.HPy_New
, of PyFT2Font
のオブジェクトのインスタン化中(つまりHPy_New
中)、HPyが構造体を割り当てて、Pythonオブジェクトとその構造体を関連づけます。構造体内で参照されるオブジェクト(name
とpy_file
)には、HPyField
として表現され、HPyField_Store
とHPyField_Load
を使ってアクセスする必要があります。HPyType_HELPERS
マクロの助けを借りて、HPy
ハンドルを使って型の構造体を取得するヘルパー関数PyFT2Font_AsStruct
を生成します。
What about NumPy usage on Matplotlib?
Matplotlibは、C APIを使って実装されたNumPyネイティブ APIやりとりします。NumPyのHPyへの移植は現在進行中なので、これは障害にはなりません。
GitHub – hpyproject/numpy-hpy
https://github.com/hpyproject/numpy-hpy
HPyには、HPy
のハンドルをPyObject*
に変換する、あるいはその逆のためのAPIが用意されています。以下はその例です。
Example of converting an HPy
handle to a PyObject *
:
// src/_path_wrapper.cpp
static HPy Py_is_sorted(
HPyContext *ctx,
HPy self,
+ HPy obj) {
- PyObject *obj) {
npy_intp size;
bool result;
PyArrayObject *array = (PyArrayObject *)PyArray_FromAny(
+ HPy_AsPyObject(ctx, obj),
- obj,
NULL, 1, 1, 0, NULL);
...
}
Example of converting a PyObject *
to an HPy
handle:
// src/ft2font_wrapper.cpp
static HPy convert_xys_to_array(HPyContext *ctx, std::vector<double> &xys)
{
npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 };
if (dims[0] > 0) {
+ return HPy_FromPyObject(ctx, PyArray_SimpleNewFromData(2, dims, NPY_DOUBLE, &xys[0]));
- return PyArray_SimpleNewFromData(2, dims, NPY_DOUBLE, &xys[0]);
} else {
+ return HPy_FromPyObject(ctx, PyArray_SimpleNew(2, dims, NPY_DOUBLE));
- return PyArray_SimpleNew(2, dims, NPY_DOUBLE);
}
}
What about missing APIs needed for Matplotlib in HPy?
移植前のHPyには、Matplotlibの重要なAPIがいくつか欠けていました。MatplotlibのソースのほとんどはC++を使って実装されており、プリプロセッシング(前処理)ルールがより制限されています。一方、HPyヘッダは、C++コンパイラと互換性のないCの機能をいくつか使っていました。そのため、HPyプロジェクトでは互換性を持たせるための調整提案が歓迎されました。
[gh-174] Add C++ support
https://github.com/hpyproject/hpy/pull/283
また、HPyのAPIにはunicode、longおよびタプル演算が欠落していましたが、これも追加されました。
Add support for more HPyUnicode_* functions
https://github.com/hpyproject/hpy/pull/291
Add support for HPyLong_(AsVoidPtr, AsDouble) & HPyErr_WriteUnraisable
https://github.com/hpyproject/hpy/pull/295
Add support for dict in HPy_BuildValue
https://github.com/hpyproject/hpy/pull/293
しかし、HPyが新しいAPIや機能を追加する基準は、上位4000のPyPi Pythonパッケージの中で、それらの機能がどれだけ一般的に使われているかに基づいています。Matplotlibは一般的に使われていない機能を使用しているため、それらの機能、例えばPyArg_ParseTuple
でネストしたタプルをパースする機能は、移植の一部としてMatplotlibに実装する必要がありました。
GitHub – hpyproject/top4000-pypi-packages: Dump of Python/C API usage in the top 4000 Python packages
https://github.com/hpyproject/top4000-pypi-packages
Support parsing nested tuples in HPyArg_Parse
https://github.com/hpyproject/hpy/pull/284
Performance
Kiwisolver
Matplotlibの依存関係としてKiwisolverが完全にHPyに移植されたので、完全移植済みのバイナリ上で、リポジトリに含まれるベンチマークを実行できました。
kiwi-hpy/benchmarks
https://github.com/hpyproject/kiwi-hpy/tree/main/benchmarks
以下の構成を使いベンチマークを実行しました。ベンチマーク実行ハードウェアおよびOSはすべて同一環境で、以下の構成です。
Intel Core i9–10885H (2.40GHz) 、Linux kernelバージョン5.10.60.1
Kiwisolver | 構成 | ビルドに利用するAPI |
---|---|---|
rev 1.3.2 | $ git checkout 1.3.2 $ python setup.py install | CPython C API |
rev HPy-1.3.2 | $ git checkout HPy-1.3.2 $ python setup.py install | CPython HPy CPython ABI |
rev HPy-1.3.2 | $ git checkout HPy-1.3.2 $ python setup.py –hpy-abi=universal install | CPython HPy Universal ABI |
以下の対話型グラフの凡例です。
- X軸:連続実行回数
- Y軸:所要時間(秒)
ご覧の通り、C APIとHPyのいずれも、性能に目立った影響を与えることなく、同じように動作しました。
Matplotlib
以下の構成のCPython 3.8の複数のモードに対して、mpl-bench basic benchmarkを実行しました。
matplotlib/mpl-bench
https://github.com/matplotlib/mpl-bench
mpl-bench/basic.py
https://github.com/matplotlib/mpl-bench/blob/master/benchmarks/basic.py
Matplotlib | 構成 | ビルドに利用するAPI |
---|---|---|
rev v3.4.x | $ git checkout v3.4.x $ python setup.py install | CPython C API |
rev HPy-V3.4.x | $ git checkout HPy-V3.4.x $ python setup.py install | CPython HPy CPython ABI |
rev HPy-V3.4.x | $ git checkout HPy-V3.4.x $ python setup.py –hpy-abi=universal install | CPython HPy Universal ABI |
以下の対話型グラフの凡例です。
- X軸:連続実行回数
- Y軸:所要時間(秒)
ベンチマーク結果でご覧の通り、MatplotlibのHPyへの移植は、C APIと比較して、CPython ABIとUniversal ABIのバイナリでは、ほぼ同じかやや上回る性能を示しています。
Performance outcome on other Python implementations
HPyのUniversal ABIは、生成されたバイナリをPyPyやGraalVM Pythonなどの様々なPython実装でロードして実行できるようにするためのポータブルなアプローチを促進します。
PyPy
https://pypy.org
Python
https://graalvm.org/python/
さらに、HPyは代替のPython実装で大幅に性能を向上させることができます。
PyPyとGraalVM Pythonはジャストインタイム(JIT)コンパイラを有しており、これにより最も頻繁に実行されるコード、つまりホットパスをその場でマシンコードにコンパイルできます。
Just-in-time compilation
https://en.wikipedia.org/wiki/Just-in-time_compilation
しかし、一般にJITコンパイラの場合、JITコンパイラがホットパスを識別し、ピーク性能に達するように機械語を生成するためのウォームアップ実行を必要とします。GraalVM PythonはHPyの最新リビジョンを採用しているため、以下の構成でKiwisolverベンチマークを実行することが可能でした。
GraalVM Python HPy Universal ABI (Sulong)
LLVMバックエンドを使用してKiwisolverのHPy (rev HPy-1.3.2)への移植版を実行しました。モジュールバイナリをビットコード形式に再コンパイルする必要がありました。
GraalVM Python HPy Universal ABI (Native)
ネイティブインターフェースバックエンドを使用してKiwisolverのHPyへの移植版(rev HPy-1.3.2)を実行しました。モジュールバイナリは、CPython HPy Universal ABIを実行したものと同じです。
このように、結果は非常に有望です。HPyへの完全な移植は、CPython上で以前と同じパフォーマンスを持つだけでなく、GraalVM PythonがCPythonと同じパフォーマンスを達成するようになったことを示しています。
一方、Matplotlibは、その計算の多くをNumPyに依存しているため、HPyへの完全な移植ではありません。NumPyのHPyへの移植は前述の通り作業中の状態であり、GraalVM Pythonはまだ完全なサポートはしていません。
GitHub – hpyproject/numpy-hpy
https://github.com/hpyproject/numpy-hpy
GraalVM Pythonでは以下の設定でbasic.time_plotを実行することができました。
GraalVM Python C API | Matplotlibの非HPyバージョン(rev v3.4.x)を実行 |
GraalVM Python HPy Universal ABI | HPyへの移植バージョンのMatplotlib (rev HPy-V3.4.x)を実行 |
両GraalVM Pythonモードでは、Matplotlibバイナリのbitcodeビルドを必要とし、GraalVM LLVM(Sulong)バックエンドを使用して実行しました。
LLVM Bitcode File Format
https://llvm.org/docs/BitCodeFormat.html
LLVM
https://en.wikipedia.org/wiki/LLVM
graal/sulong
https://github.com/oracle/graal/tree/master/sulong
以下のグラフは、ベンチマーク結果を示しています。
グラフからわかるように、GraalVM PythonはC APIと比較して、HPy版のMatplotlibで大幅に性能が向上しました。MatplotlibのHPy実装は、ピーク時とウォームアップ時の両方で、C APIよりも約4倍高速でした。しかし、GraalVM Pythonは、NumPyのC API実装のため、このベンチマークではまだCPythonより遅いです。
Final note
この記事で、モジュール内でHPyに移行することの利点をご理解いただけたと思います。HPy は CPython から切り離し、他の実装をターゲットにする可能性を提供するだけでなく、素晴らしいパフォーマンスをもたらす可能性があります。
Florian Angerer、Alina Yurenko、Bernard Horanに感謝します。