- 著者は Zig言語を学びながら AcoustID のインデックス再構築プロジェクトを進める中で、ネットワークプログラミングの限界をきっかけに新しいアプローチを試みた
- 既存の C++ と Go で使っていた 非同期 I/O と並行性モデルを Zig でも実装するため、独自ライブラリの開発を決意した
- その結果、Go スタイルの並行性モデルを Zig 向けに実装した Zio ライブラリを作成し、コールバックなしで同期的に見える非同期コードを書けるようにした
- Zio は 非同期ネットワーク・ファイル I/O、チャネル、同期プリミティブ、シグナル監視などをサポートし、シングルスレッドモードでは Go や Rust の Tokio より高速な性能を示した
- このプロジェクトは Zig の システムレベル性能と現代的な並行性モデルの組み合わせ可能性を示し、Zig エコシステム拡大の重要な転換点と評価されている
Zig 言語と初期の動機
- 著者はもともと オーディオソフトウェア向けの低レベル言語として設計された Zig を見守っていたが、実際の必要性は感じていなかった
- Zig の創設者 Andrew Kelley が著者の Chromaprint アルゴリズムを Zig で再実装した事例を見て興味を持つようになった
- AcoustID の 逆インデックス再構築プロジェクトを Zig 学習の機会として進め、結果として C++ 版より 高速で拡張性の高い実装を達成した
- しかしサーバーインターフェース追加の段階で、非同期ネットワーキング支援の不足という問題に直面した
既存のアプローチと限界
- 以前の C++ 版では Qt フレームワークを使って非同期 I/O を処理しており、コールバックベースではあるものの、豊富なサポートのおかげで利用可能だった
- その後のプロトタイプでは Go 言語のネットワーキングと並行処理の扱いやすさを活用したが、Zig には同程度の抽象化が存在しなかった
- Zig で TCP サーバーとクラスタ層を実装しようとすると、多数のスレッドを生成しなければならない非効率が発生した
- これを解決するため、NATS メッセージングシステム向けの Zig クライアント (
nats.zig) を自ら実装し、Zig のネットワーキング機能を深く掘り下げた
Zio ライブラリの登場
- こうした経験をもとに、Zio: Zig 向け非同期 I/O および並行性ライブラリを公開した
- Zio は コールバックなしの非同期コード記述を目標としており、内部では非同期 I/O が動作しつつ、外からは同期的に見える構造を持つ
- Go スタイルの並行性モデルを Zig に合わせて限定的に実装している
- Zio のタスクは 固定サイズのスタックを持つスタックフルコルーチンの形を取る
stream.read() を呼び出すと I/O 処理がバックグラウンドで行われ、完了時にタスクが再開されて結果が返る
- この方式は 状態管理の単純化と コード可読性の向上を同時にもたらす
機能構成とランタイム構造
- Zio は 完全な非同期ネットワークおよびファイル I/O、同期プリミティブ(mutex、条件変数など)、Go スタイルのチャネル、OS シグナル監視などをサポートする
- タスクは シングルスレッドまたはマルチスレッドモードで実行できる
- マルチスレッドモードではタスクがスレッド間を移動可能で、レイテンシ低減と負荷分散の向上という効果がある
- 標準 Reader/Writer インターフェースを実装し、外部ライブラリとの互換性を確保している
性能と比較
- 著者はまだ公式ベンチマークを公開していないが、シングルスレッドモードで Go と Rust の Tokio より高速な性能を確認したと述べている
- コンテキストスイッチングのコストが関数呼び出しレベルまで低く、事実上無料に近い切り替え速度を提供する
- マルチスレッドモードはまだ Go/Tokio ほど堅牢ではないが、同程度かやや高速な性能を示す
- 今後 公平性(fairness) 機能を追加した場合、性能が一部低下する可能性がある
サンプルコードと活用
- ドキュメントには Zio ベースの HTTP サーバーのサンプルコードが含まれている
zio.net.Stream を使って接続を受け付け、各接続を個別タスクとして処理する
zio.Runtime がタスク実行と I/O スケジューリングを管理する
- この構造により、非同期 I/O を同期コードのように記述でき、明確なフロー制御とリソース解放管理が可能になる
今後の計画と意義
- 著者は Zio を通じて、Zig が単なる 高性能システムコード向け言語を超え、完全なネットワークアプリケーション開発言語へと発展できることを確認した
- 次の段階として、NATS クライアントを Zio ベースで再実装し、Zio ベースの HTTP クライアント/サーバーライブラリの開発を計画している
- このプロジェクトは Zig エコシステムのネットワーキング・並行性インフラ拡張を主導し、Go や Rust に比肩する 現代的ランタイムモデル構築の試みとして評価されている
1件のコメント
Hacker News の意見
Zig の async 設計がハードウェアの call/return ペアを使うのか、それとも間接ジャンプベースに変換されるのかははっきりしない
完璧なベンチマークを行うには、2つのタスク間で継続的なスイッチングがあるプログラムと、完全に同期的なプログラムの総実行時間を比較する必要がある。これはかなり難しい
コンパイラを制御できるなら、I/O コードの call/ret を明示的なジャンプに置き換えることも可能だ
長期的には、CPU がスタックフルコルーチンをより正確に予測できるよう、**メタ予測器(meta-predictor)**が導入されてほしい
関連記事: Zig new async I/O
私は Zig を組み込み環境(ARM Cortex-M4、256KB RAM)で使っており、C との相互運用でメモリ安全性を確保するために使っている
Rust のような色付き async のほうが好みだ。同期コードのように見える魔法っぽさは良いが、大きなコードベースではどの関数が blocking なのか見分けにくくなるのが問題だ
CPU は I/O で本当にブロックされるわけではなく、OS スレッド自体が OS によって実装されたスタックフルコルーチンだ
言語レベルでこの幻想をより効率的に実装できるだけで、本質は同じだ
関数が I/O を行うかどうかで色が決まり、呼び出し時点で async かどうかを明示する
Zig は関数呼び出し時に必要なスタックサイズを計算する機能も目指しており、スタックフルコルーチンのRAM 浪費問題を減らせるのではないかと期待されている
それでもプロジェクトは依然として活発で、早いリリースより正しい設計を優先している点は好意的に見ている
今は Go や C を使いながら 1.0 を待っている
私も I/O 中心の作業のために 0.16 を待つ予定だ
既存コードはそのまま動き、新 API はより人間工学的で性能も高い
私も既存プロジェクトを新しい Reader/Writer API に移行したが、コードがずっとすっきりした
libtask のようなアプローチのほうがずっときれいに見える
Rust もコールバックベース async を採用したが、その理由がよくわからない
参考: libtask
しかしスタックを直接扱うと、例外処理、GC、デバッガなどと衝突することがある
LLVM レベルでこうした変更をマージするのも難しいため、言語設計者の立場では現実的な制約が多い
小さすぎればオーバーフローし、大きすぎればメモリの無駄になる
プラットフォームごとに必要なスタックサイズが異なるため、移植性の問題もある
Zig の issue #157 が解決されれば、このアプローチはもっと良くなると思う
つまり、async を実装する方法は3つある
Rust は静的な状態マシンに変換され、ランタイムがそれをポーリングする
スタックフルはメモリ浪費が大きく、スタックサイズ管理も難しい
Rust はこれを避けるためにスタックレス構造を採用し、Zig は両方の方式を選べるようにする予定だ
参考: zio coroutine コード
setsockoptで読み取り/書き込みタイムアウトを設定できるZig は POSIX API レイヤーを提供している
参考: setsockopt ドキュメント
Python の
asyncio.timeoutのように動作する構造を考えているサンプルコード:
実際にはそこがいちばん難しい部分だ
参考: zio.dev
しかし Zig は低レベル言語でありながら高レベル APIをきれいに表現できるので印象的だった
Zig と Go の両方にQt バインディングが新しくできた
Rust 向けのバインディングが欲しい。cxx-qt が唯一メンテされているプロジェクトだが、QML や CMake は使いたくない。Rust + Cargoだけで Qt を使いたい