2 ポイント 投稿者 GN⁺ 2025-10-28 | 1件のコメント | WhatsAppで共有
  • 著者は 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件のコメント

 
GN⁺ 2025-10-28
Hacker News の意見
  • コンテキストスイッチングは関数呼び出しレベルでほぼ無料だと言われるが、実際にはブランチ予測器(branch predictor)が壊れるなどの微妙なコストがある
    Zig の async 設計がハードウェアの call/return ペアを使うのか、それとも間接ジャンプベースに変換されるのかははっきりしない
    完璧なベンチマークを行うには、2つのタスク間で継続的なスイッチングがあるプログラムと、完全に同期的なプログラムの総実行時間を比較する必要がある。これはかなり難しい
    • スタックレスコルーチンで、呼び出しスタックの最下部で2つのタスクをずっと切り替え、スタック切り替えコードがインライン化されていれば、call/ret ミスマッチのペナルティは大半を回避できる
      コンパイラを制御できるなら、I/O コードの call/ret を明示的なジャンプに置き換えることも可能だ
      長期的には、CPU がスタックフルコルーチンをより正確に予測できるよう、**メタ予測器(meta-predictor)**が導入されてほしい
    • Zig では現在 async が言語機能としていったんなくなっており、OP が自分でユーザー空間でタスクスイッチングを実装したものだ
    • 単純なコルーチン間 ping-pong テストをしたとき、他のソリューションと比べて信じがたい数値が出たことがある
    • Zig にはまもなく新しい async が追加される予定なので、本格的に掘り下げる前に待っているところだ
      関連記事: Zig new async I/O
  • スタックフルコルーチンは、RAM が十分あるときに意味がある
    私は Zig を組み込み環境(ARM Cortex-M4、256KB RAM)で使っており、C との相互運用でメモリ安全性を確保するために使っている
    Rust のような色付き async のほうが好みだ。同期コードのように見える魔法っぽさは良いが、大きなコードベースではどの関数が blocking なのか見分けにくくなるのが問題だ
    • 実際のところ、すべての同期コードはソフトウェアが作った**幻想(illusion)**にすぎない
      CPU は I/O で本当にブロックされるわけではなく、OS スレッド自体が OS によって実装されたスタックフルコルーチンだ
      言語レベルでこの幻想をより効率的に実装できるだけで、本質は同じだ
    • 新しい Zig IO は Rust よりも洗練された形の色付き(colored)構造になる予定だ
      関数が I/O を行うかどうかで色が決まり、呼び出し時点で async かどうかを明示する
      Zig は関数呼び出し時に必要なスタックサイズを計算する機能も目指しており、スタックフルコルーチンの
      RAM 浪費問題
      を減らせるのではないかと期待されている
    • だからこそ Zig が I/O を明示的に表現しようとしているのは、どの関数がblocking か追跡できるようにするためだ
  • Zig を今導入するのは時期尚早だという意見がある。I/O モデルが大きく変わっている最中で、数年はかかりそうな印象だ
    • 私も 2020 年に似た理由で Zig を離れた。
      それでもプロジェクトは依然として活発で、早いリリースより正しい設計を優先している点は好意的に見ている
      今は Go や C を使いながら 1.0 を待っている
    • 数年なんてあっという間だ。Zig はすでに十分実用的な言語だ。使う人は使うし、使わない人は使わない
    • 実際には悪いタイミングではある。0.16 で大きな I/O 変更が予定されており、著者自身も最新機能をまだ使っていない
      私も I/O 中心の作業のために 0.16 を待つ予定だ
    • ただし I/O 関連の作業なら、Zig 0.15 のバッファード Reader/Writer インターフェースを使えば大きな変化はないはずだ
    • むしろ今がだめだとは思わない。Zig は言語自体が激変しているわけではなく、新しく強力な std.Io APIを追加しているところだ
      既存コードはそのまま動き、新 API はより人間工学的で性能も高い
      私も既存プロジェクトを新しい Reader/Writer API に移行したが、コードがずっとすっきりした
  • コールバックベース async が標準になった理由はいまだに疑問
    libtask のようなアプローチのほうがずっときれいに見える
    Rust もコールバックベース async を採用したが、その理由がよくわからない
    参考: libtask
    • スタックレスコルーチンは言語内部で実装でき、既存機能との予測可能な相互作用が利点だ
      しかしスタックを直接扱うと、例外処理、GC、デバッガなどと衝突することがある
      LLVM レベルでこうした変更をマージするのも難しいため、言語設計者の立場では現実的な制約が多い
    • Microsoft が C++ 標準向けに行った研究では、スタックレスコルーチンのほうがメモリオーバーヘッドがはるかに小さく、エグゼキュータ設計の自由度も高いという結論になった
    • zio や libtask 方式の欠点は、スタックサイズを自分で見積もらなければならないこと
      小さすぎればオーバーフローし、大きすぎればメモリの無駄になる
      プラットフォームごとに必要なスタックサイズが異なるため、移植性の問題もある
      Zig の issue #157 が解決されれば、このアプローチはもっと良くなると思う
    • libtask のような場合、スレッドスタックサイズが曖昧で、一般的な async の状態よりはるかに大きい
    • Rust の async はコールバックではなく**ポーリング(polling)**ベースだ
      つまり、async を実装する方法は3つある
      1. コールバックベース(Node.js、Swift)
      2. スタックフルベース(Go、libtask)
      3. ポーリングベース(Rust)
        Rust は静的な状態マシンに変換され、ランタイムがそれをポーリングする
        スタックフルはメモリ浪費が大きく、スタックサイズ管理も難しい
        Rust はこれを避けるためにスタックレス構造を採用し、Zig は両方の方式を選べるようにする予定だ
        参考: zio coroutine コード
  • TCP 読み取りが1か月ブロックされることもあり得るが、I/O タイムアウトインターフェースはどうなるのか気になる
    • TCP ソケットでは setsockopt で読み取り/書き込みタイムアウトを設定できる
      Zig は POSIX API レイヤーを提供している
      参考: setsockopt ドキュメント
    • 現在の Zig の std.Io.Reader はタイムアウトを認識しない
      Python の asyncio.timeout のように動作する構造を考えている
      サンプルコード:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • 多くの async フレームワークはタイムアウトとキャンセルを軽視している
      実際にはそこがいちばん難しい部分だ
  • Scala にはすでに ZIO という並行性ライブラリがある
    参考: zio.dev
  • 最近 Rust の Tokio に感銘を受けたが、Zig でもGo スタイルの並行性を GC なしで実現できるならぜひ使ってみたい
    • Go は 無限に拡張可能なスタックのようなトリックを GC のおかげで使える
      しかし Zig は低レベル言語でありながら高レベル APIをきれいに表現できるので印象的だった
  • Zig を初めて知ったのは Bun のウェブサイトだった。最近本当に急速に進化している
  • 以前の C++ バージョンでは Qt で非同期 I/O を実装していたが、今回は Go に切り替えた
    Zig と Go の両方にQt バインディングが新しくできた
    • Go: miqt
    • Zig: libqt6zig
      Rust 向けのバインディングが欲しい。cxx-qt が唯一メンテされているプロジェクトだが、QML や CMake は使いたくない。Rust + Cargoだけで Qt を使いたい
  • Scala にもすでに有名なフレームワーク ZIO があるので、名前を付けるのは本当に難しいと感じる