PythonをZigでスピードアップする
おもしそうな動画を見かけたのでフォロー
当方、英語力/C (Zig)言語力が怪しいのでツッコミコメント大歓迎です。
ZigでPythonライブラリを作成するという試み。 (GitHub)
PyCon Deでの登壇のようです。
Zigの紹介から、Zigのメリットを紹介してくれています。
以下動画内のスライドの内容。
斜体の箇所が登壇者 Adamさんのコメントです。
Why Zig?
Import from C Header File
@cImport
で.h
ファイルを直接インポートできるよ- Python C APIのインポートが簡単
const py = @cImport({
@cDefine("PY_SSIZE_T_CLEAN", {});
@cInclude("Python.h");
});
Other conveniences for interacting with C
- 呼び出し規約 (Calling conventions) を使えばCから呼び出すための関数も簡単に作れるよ
Zig's primitive types include specific-types for ABI compatibility with C
- Zigのプリミティブ型にはCとABI互換があるよ
C pointers
- Cのポインターもあるよ。でも可能な限り使うのは避けた方が良いよ。Cのコードから変換したとかでなければ。
なぜZigのメリットとしてあげたのだろう?
Zig even comes with a command to automatically translate C code
- Cのコードから変換できるCLIツールがあるよ
続いてなぜC言語でなくZigを紹介したかとの話題に。
C言語によるPythonの高速化はやっぱり難しいからみたいなことを言ってる?
Why not just C?
Rich and comprehensive standard library.
- Zigだとモダンで便利なライブラリがそろっているよ
Official package manager. Soon.
- もうすぐ公式のパッケージマネージャーがくるよ (1.0にあがったらね)
Cross-platform tool chain and build system.
-
クロスプラットフォームのツールセットやビルドシステムに積極的に取り組んでいるよ
- https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html
- Wineを使ってWindowsでも実行できる!
Zigのツールセットかなり優秀そう
No macros, various warts removed, compile time execution, Optional types (discourage null pointers), modern conveniences... etc.
zig zen
- Pythonと哲学・文化が似ていると思うよ
- zen of Zig見てみてね
長い紹介はここまでにして、早速コードを見て行くことに。
3つのPRにこれからやることをまとめているよ とのこと。
- The simplest possible C-extension for Python, with CI pipeline
- The simplest possible Zig extension for Python
- Benchmark of YAML parsing for Python using zig-yaml
まず初めにREADMEに登壇者の目的を書いているとのことで紹介。
- Pure Zigで実装するよ、そしてFFIは使わず直接 Python.h をインポートするよ
- Zigのツールチェーンだけを使ってコンパイルするよ (clang とかは使わないよ)
- 発表時点ではmacOSXとLinuxで確認したよ
続いてCでの実装とZigでの実装をコードを見せつつ紹介。
Zigではマクロが使えない分、一部Cより記載が増えている。
ただ、構文は流石にモダンな印象で、未使用変数がエラーになったりNull安全があったり堅牢性が高そう。
以下、勉強がてら写経。C(やもちろんZigも)で拡張モジュールを書いたことがなかったので、ただ書き写すだけでも勉強になる。
Cでの実装
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
zaml_load(PyObject *self, PyObject *args)
{
return Py_BuildValue("i", 1);
}
static PyMethodDef ZamlMethods[] = {
{.ml_name = "load",
.ml_meth = zaml_load,
.ml_flags = METH_NOARGS,
.ml_doc = "Load some tasty YAML."},
{.ml_name = NULL,
.ml_meth = NULL,
.ml_flags = 0,
.ml_doc = NULL}, /* Sentinel */
};
static struct PyModuleDef zamlmodule = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "zaml", /* name of module */
.m_doc = NULL, /* module documentation, may be NULL */
.m_size = -1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
.m_methods = ZamlMethods,
.m_slots = NULL,
.m_traverse = NULL,
.m_clear = NULL,
.m_free = NULL,
};
PyMODINIT_FUNC
PyInit_zaml(void)
{
return PyModule_Create(&zamlmodule);
}
from setuptools import setup, Extension
zaml = Extension("zaml", sources=["zamlmodule.c"])
setup(
name="zaml",
version="0.0.1",
description="Fast YAML 1.2 Parser for Python 3.10.x",
ext_modules=[zaml],
)
$ pip install -e .
Obtaining file:///work
Installing collected packages: zaml
Running setup.py develop for zaml
Successfully installed zaml```
$ ipython
In [1]: import zaml
In [2]: zaml.load()
Out[2]: 1
Zigでの実装
const py = @cImport({
@cDefine("PYSSIZE_T_CLEAN", {});
@cInclude("Python.h");
});
const PyObject = py.PyObject;
const PyMethodDef = py.PyMethodDef;
const PyModuleDef = py.PyModuleDef;
const PyModuleDef_Base = py.PyModuleDef_Base;
const Py_BuildValue = py.Py_BuildValue;
const PyModule_Create = py.PyModule_Create;
const METH_NOARGS = py.METH_NOARGS;
fn zaml_load(self: ?*PyObject, args: ?*PyObject) callconv(.C) *PyObject {
_ = self;
_ = args;
return Py_BuildValue("i", @as(c_int, 1));
}
var ZamlMethods = [_]PyMethodDef{
PyMethodDef{
.ml_name = "load",
.ml_meth = zaml_load,
.ml_flags = METH_NOARGS,
.ml_doc = "Load some tasty YAML.",
},
PyMethodDef{
.ml_name = null,
.ml_meth = null,
.ml_flags = 0,
.ml_doc = null,
}
};
var zamlmodule = PyModuleDef{
.m_base = PyModuleDef_Base{
.ob_base = PyObject{
.ob_refcnt = 1,
.ob_type = null,
},
.m_init = null,
.m_index = 0,
.m_copy = null,
},
.m_name = "zaml",
.m_doc = null,
.m_size = -1,
.m_methods = &ZamlMethods,
.m_slots = null,
.m_traverse = null,
.m_clear = null,
.m_free = null,
};
pub export fn PyInit_zaml() *PyObject {
return PyModule_Create(&zamlmodule);
}
Zigのモジュールをビルドするため setup.py
もひと工夫が必要。
サンプルそのままコピー[1]でもビルドできた。
setup.py
とbuilder.py
の2ファイル。Pythonからzigコマンドを外部実行してビルドしている。その際Python.h
をインクルードパスに追加している。
-
Python側でZigのビルド設定している(Zig側でビルド設定していない)ので実際に開発する際にはもうひと工夫した方が良さそう。
Python.h
が認識されないのでそのままだとzigビルド時やzls利用時にでエラーになる。 ↩︎
閑話休題
動画の内容からは離れ、開発体験向上のため build.zig
を作成してみた。これでzlsとかで補完などが効くようになった。[1]
サンプルの builder.py
となるべく合わせるため Zigのコード でコマンドオプションを探しながらAPIを特定していく作業…
ひとまずビルドは通るようになったが、出力されるlibファイル名が合わせられなかった(ここまでできたので満足はしている) [2]
build.zig
Pythonのヘッダファイルを取得するためPythonコマンドを実行するという力技。
build時に --verbose
オプションをつけると実行コマンドも出力してくれるので確認に役立った。
const std = @import("std");
const ChildProcess = std.ChildProcess;
const EmitOption = std.EmitOption;
pub fn build(b: *std.build.Builder) !void {
// Find Python header file
const result = try ChildProcess.exec(.{
.allocator = std.heap.page_allocator,
.argv = &[_][]const u8{
"python",
"-c",
"import sysconfig; print(sysconfig.get_path(\"include\"), end=\"\")",
},
});
const includeDir = result.stdout;
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const lib = b.addSharedLibrary("zaml", "zamlmodule.zig", .unversioned);
lib.addIncludeDir(includeDir);
lib.setBuildMode(mode);
lib.linkage = .dynamic;
lib.linker_allow_shlib_undefined = true;
lib.linkSystemLibraryName("c");
// FIXME: Match the out file of setup.py!!
// const target = lib.target_info.target;
// const arch = target.cpu.arch;
// const os = target.os.tag;
// const version = target.os.getVersionRange();
// lib.emit_bin = .{ .emit_to = path };
lib.install();
const main_tests = b.addTest("zamlmodule.zig");
main_tests.addIncludeDir(includeDir);
main_tests.setBuildMode(mode);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}
続いてベンチマークのお話に。
とあるWebサイトのバックエンドをPythonで実装してたが、巨大なyamlファイルを扱うとただパースするだけで遅いことがネックだったとのこと。[1]
そこでPoCを行い試してみることにしたそう。
100万行のyamlファイルを読み込みをzaml (written in Zig), PyYAML (in C), ruamel, PyYAMLの4つで比較
結果はZigで実装のPythonモジュール、zamlが圧倒的に速い結果に。
PyYAMLとZigで実装内容が違うので、単にZigの実装が速いだけという可能性も十分にあるが、結果としておもしろい。
Running benchmarks...
Benchmark results:
zaml took 0.89 seconds
PyYAML CSafeLoader took 13.36 seconds
ruamel took 38.86 seconds
PyYAML SafeLoader took 81.78 seconds
from README.md of zaml
-
C = Cythonで書かれているので早いはず ↩︎
最後にまとめ。
Conclusions
- We can and should continue to improve performance of the Python library ecosystem.
- Zig can help us do that.
質疑応答パート。
なぜ「Zigなの?」というところに質問が来ていた。技術的な理由というよりシンプルにZigがおもしろそうだから紹介したという方が強そう。
CythonやRustでなくなぜZigなの?
- ZigのPhilosophyが気に入ったよ
- Cythonなどはすでに色々なライブラリがあるけどそれとは違うものを紹介したかったよ
- Zigはとてもおもしろい資質があると思うよ
- まさにZig開発者の「Cを置き換える」という言葉
このライブラリを商用環境で使う?もしそうならチームパフォーマンス (Velocity) にどれくらいの影響が出そう?
- 今は商用環境で使う予定はないよ
- でも個人的にはいつか商用環境で使えるようにしたいな
Zigの "おもしろい資質" ってなに?
-
Cと違うところ
- Cにはない安全な特徴があったり
comptime
があったり- メモリアロケーたがいい感じ
- Zig開発者のAndrew Kelleyを尊敬しているから!
まとめ
最後に個人的な感想です。
登壇を拝見してですが、実際にZigでPythonモジュールを書くのは 結構相性が良さそう に感じました。
Cよりも実装もしやすく、ツールチェーンが豊富でまさにCライブラリのように扱えるので組み込みもしやすい。
もっと流行らないかなーという印象です。
ぜひ皆さんもZigで書いてみてください!
おまけ
まさかのご本人(登壇者のAdamさん)に拾ってもらいましたw