Closed12

PythonをZigでスピードアップする

kama-meshikama-meshi

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ツールがあるよ
kama-meshikama-meshi

続いてなぜ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.

Zigのツールセットかなり優秀そう

No macros, various warts removed, compile time execution, Optional types (discourage null pointers), modern conveniences... etc.

zig zen

  • Pythonと哲学・文化が似ていると思うよ
  • zen of Zig見てみてね
kama-meshikama-meshi

長い紹介はここまでにして、早速コードを見て行くことに。
3つのPRにこれからやることをまとめているよ とのこと。

  1. The simplest possible C-extension for Python, with CI pipeline
  2. The simplest possible Zig extension for Python
  3. Benchmark of YAML parsing for Python using zig-yaml

まず初めにREADMEに登壇者の目的を書いているとのことで紹介。

  • Pure Zigで実装するよ、そしてFFIは使わず直接 Python.h をインポートするよ
  • Zigのツールチェーンだけを使ってコンパイルするよ (clang とかは使わないよ)
  • 発表時点ではmacOSXとLinuxで確認したよ
kama-meshikama-meshi

続いてCでの実装とZigでの実装をコードを見せつつ紹介。
Zigではマクロが使えない分、一部Cより記載が増えている。
ただ、構文は流石にモダンな印象で、未使用変数がエラーになったりNull安全があったり堅牢性が高そう。

以下、勉強がてら写経。C(やもちろんZigも)で拡張モジュールを書いたことがなかったので、ただ書き写すだけでも勉強になる。

Cでの実装
zamlmodule.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);
}
setup.py
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での実装
zamlmodule.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.pybuilder.py の2ファイル。Pythonからzigコマンドを外部実行してビルドしている。その際 Python.h をインクルードパスに追加している。

脚注
  1. Python側でZigのビルド設定している(Zig側でビルド設定していない)ので実際に開発する際にはもうひと工夫した方が良さそう。 Python.h が認識されないのでそのままだとzigビルド時やzls利用時にでエラーになる。 ↩︎

kama-meshikama-meshi

閑話休題

動画の内容からは離れ、開発体験向上のため build.zig を作成してみた。これでzlsとかで補完などが効くようになった。[1]
サンプルの builder.py となるべく合わせるため Zigのコード でコマンドオプションを探しながらAPIを特定していく作業…

ひとまずビルドは通るようになったが、出力されるlibファイル名が合わせられなかった(ここまでできたので満足はしている) [2]

build.zig

Pythonのヘッダファイルを取得するためPythonコマンドを実行するという力技。
build時に --verbose オプションをつけると実行コマンドも出力してくれるので確認に役立った。

build.zig
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);
}
脚注
  1. ちなみにVS Codeだと zig.buildArgs-I オプションでPythonヘッダのパスを追加するのでもOK ↩︎

  2. arch情報などはとれたが、OS名が文字列として取得できず ↩︎

kama-meshikama-meshi

続いてベンチマークのお話に。
とある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

脚注
  1. C = Cythonで書かれているので早いはず ↩︎

kama-meshikama-meshi

最後にまとめ。


Conclusions

  • We can and should continue to improve performance of the Python library ecosystem.
  • Zig can help us do that.
kama-meshikama-meshi

質疑応答パート。
なぜ「Zigなの?」というところに質問が来ていた。技術的な理由というよりシンプルにZigがおもしろそうだから紹介したという方が強そう。


CythonやRustでなくなぜZigなの?

  • ZigのPhilosophyが気に入ったよ
  • Cythonなどはすでに色々なライブラリがあるけどそれとは違うものを紹介したかったよ
  • Zigはとてもおもしろい資質があると思うよ
  • まさにZig開発者の「Cを置き換える」という言葉

このライブラリを商用環境で使う?もしそうならチームパフォーマンス (Velocity) にどれくらいの影響が出そう?

  • 今は商用環境で使う予定はないよ
  • でも個人的にはいつか商用環境で使えるようにしたいな

Zigの "おもしろい資質" ってなに?

  • Cと違うところ
    • Cにはない安全な特徴があったり
    • comptime があったり
    • メモリアロケーたがいい感じ
  • Zig開発者のAndrew Kelleyを尊敬しているから!
kama-meshikama-meshi

まとめ

最後に個人的な感想です。

登壇を拝見してですが、実際にZigでPythonモジュールを書くのは 結構相性が良さそう に感じました。
Cよりも実装もしやすく、ツールチェーンが豊富でまさにCライブラリのように扱えるので組み込みもしやすい。
もっと流行らないかなーという印象です。

ぜひ皆さんもZigで書いてみてください!

このスクラップは2022/09/30にクローズされました