← 記事一覧に戻る
March 27, 2026
読了時間: 5分

ZigBolt:なぜ我々はZigで独自のAeronを構築し、メッセージあたり20ナノ秒を達成したのか

ZigBolt:なぜ我々はZigで独自のAeronを構築し、メッセージあたり20ナノ秒を達成したのか
#zigbolt
#zig
#hft
#低レイテンシ
#メッセージング
#aeron
#ipc
#オープンソース

ZigBolt — 超低レイテンシメッセージング ロックフリーリングバッファ、ゼロコピーコーデック、Raftクラスタ——すべて純粋なZigで、すべてオープンソース。

アルゴリズムトレーディングやマーケットメイキングに携わっているなら、1マイクロ秒の価値をご存知でしょう。たった1回の余分なコンテキストスイッチで、あなたの注文は2番目に到着します。JVMのガベージコレクションが一度停止するだけで、反対側のマーケットメイカーはすでにクォートを更新しています。ナノ秒単位で利益が決まる世界では、メッセージングインフラはサービス間の退屈なパイプラインではなく、競争優位そのものです。

我々はZigBoltを構築しました——Zig言語による高頻度取引向けメッセージングシステムです。ゼロから。JVMなし、ガベージコレクタなし、Media Driverなし、XML設定ファイルなし。そして、SPSCリングバッファでp50レイテンシ20ナノ秒、共有メモリIPCで30ナノ秒を達成しました。

本記事では、なぜこれが必要だったのか、内部の仕組み、そしてなぜZigを選んだのかについて解説します。


概要

  • ZigBolt — 純粋なZigで書かれたHFT向けオープンソース(MIT)メッセージングシステム
  • SPSC p50: 20nsIPC p50: 30ns — Aeronの公称スペックを上回る
  • ゼロコピーコーデックは0nsで動作(コンパイル時コード生成、ランタイムは単なるポインタキャスト)
  • GCなし、JVMなし、Media Driverなし — ライブラリはアプリケーションに直接組み込み
  • Raftクラスタ、アーカイブ、シーケンサ — すべて同梱
  • FFIバインディング:Rust、Python、Go、TypeScript、C対応 — お好みの言語で

課題:Aeronは優れているが、十分ではない

Real LogicのAeronは、キャピタルマーケットにおける低レイテンシメッセージングのデファクトスタンダードです。数十のHFT企業が利用し、実戦で検証済みで、優れたアーキテクチャを持っています。しかしAeronには根本的な問題があります——JVMです。

JVM safepoints:見えない敵

すべてのデータを慎重にオフヒープメモリに配置し、GCエルゴノミクスを無効にしてGuaranteedSafepointInterval=300000を設定しても、JVMは依然として時折すべてのスレッドをsafepointで停止させます。これはバグではなく、アーキテクチャ上の設計です。JVMはデオプティマイゼーション、バイアスドロッキング、スタックウォーキングのためにsafepointsを必要とします。

実際にはこうなります:スレッドがp50 = 200nsでメッセージを送信しているのに、突然p99.9が50μsに跳ね上がる。明確な原因なしに。JVMのいずれかのスレッドが、safepointの時間だと判断したからです。

Media Driver:余分なホップ

Aeronは Media Driver——パブリッシャーとサブスクライバー間で共有メモリを介してメッセージをルーティングする独立プロセス(または組み込みJVM)——を通じて動作します。これにより美しいアイソレーションが実現されますが、最低1回の余分なホップが追加されます:

Aeron:    App → shm → Media Driver → shm → socket → NIC
ZigBolt:  App → ring buffer → io_uring → NIC

各ホップごとに、余分なナノ秒、余分なキャッシュミス、余分な不確定性が加わります。

SBE:別途のビルドステップ

Simple Binary Encodingは、金融メッセージの標準的なFIXコーデックです。Aeronエコシステムでは、XMLスキーマからコードを生成する独立したJavaユーティリティです。別途の依存関係、別途のビルドステップ、別途の問題群。


ソリューション:ZigBolt

我々は自問しました:Aeronの最良のアイデア——トリプルバッファログ、ロックフリーリングバッファ、Raftクラスタ——を、以下の特性を持つ言語で実装したらどうなるか:

  1. ランタイムオーバーヘッドなし(GCなし、safepointsなし)
  2. コンパイル時にコード生成が可能(comptime)
  3. Cライブラリ(DPDK、io_uring)との統合が容易
  4. バイナリサイズ約100KB

その言語とは——Zigです。


アーキテクチャ

┌─────────────────────────────────────────────────────────┐
│  Publisher/Subscriber API(型付きラッパー)                │
├─────────────────────────────────────────────────────────┤
│  Transport Layer(チャネルファクトリ、ライフサイクル)        │
├─────────────────────────────────────────────────────────┤
│  IPC Channel(共有メモリ)    │ UDP Channel(ネットワーク) │
├─────────────────────────────────────────────────────────┤
│  WireCodec(comptime ゼロコピー)│ SBE Encoder/Decoder    │
├─────────────────────────────────────────────────────────┤
│  Ring Buffers (SPSC/MPSC) │ LogBuffer(トリプルバッファ)  │
├─────────────────────────────────────────────────────────┤
│  Archive(リプレイ)│ Sequencer(全順序)│ Raft(HA)       │
└─────────────────────────────────────────────────────────┘

7層構造で、各層を独立して使用可能です。2つのプロセス間のIPCにSPSCリングバッファだけが必要?そのまま使えます。Raftコンセンサスとアーカイブを備えたフルクラスタが必要?それもあります。


ベンチマーク:言葉ではなく数字で

ZigBoltベンチマーク結果

以下は1000万回のイテレーション(Apple Silicon / macOS)における実測結果です:

SPSCリングバッファ

メッセージサイズ p50 p99 p99.9 スループット
8バイト 20ns 30ns 120ns 42.8M msg/s
32バイト 30ns 50ns 150ns 28.5M msg/s
64バイト 50ns 60ns 320ns 17.6M msg/s
256バイト 30ns 50ns 50ns 29.5M msg/s

IPC Channel(共有メモリ)

メッセージサイズ p50 p99 p99.9 スループット
64バイト 30ns 40ns 40ns 35.7M msg/s
256バイト 40ns 40ns 170ns 27.4M msg/s
1024バイト 90ns 260ns 900ns 9.9M msg/s

LogBuffer(Aeron方式トリプルバッファ)

メッセージサイズ p50 p99 p99.9 スループット
32バイト 30ns 40ns 320ns 33.6M msg/s
64バイト 30ns 30ns 160ns 38.0M msg/s
256バイト 30ns 40ns 60ns 31.1M msg/s

WireCodec(comptimeゼロコピー)

操作 レイテンシ スループット
エンコード(32バイト) 0ns ∞(インラインmemcpy)
デコード(32バイト) ~0.4ns 27億 msg/s

はい、読み間違いではありません:エンコードはゼロナノ秒です。なぜならWireCodec(T)はコンパイル時に構造体を検証し、encode/decodeを通常の@memcpyまたはポインタキャストに変換するからです。ランタイムオーバーヘッド = ゼロ。

比較のために:AeronはIPC RTT(ラウンドトリップ)約250nsと公表しています。我々のワンウェイレイテンシは30ns。ラウンドトリップで計算しても、4倍高速です。


内部の仕組み

ロックフリーSPSC:シンプルさという美徳

SPSCリングバッファ

単一プロデューサー・単一コンシューマーのリングバッファは、最もシンプルかつ最速のロックフリーデータ構造です。ライターがheadを進め、リーダーがtailを進めます。CASは不要——acquire/releaseアトミックで十分です。

重要なトリックはキャッシュラインパディングです。headtailが同一キャッシュラインに存在すると、一方のカウンタの更新がもう一方のコアのキャッシュを無効化します(フォールスシェアリング)。解決策:

// Head(書き込み位置)——独自のキャッシュライン
head: std.atomic.Value(usize) align(128) = .init(0),

// 128バイトパディング——アイソレーションを保証
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,

// Tail(読み取り位置)——独自のキャッシュライン
tail: std.atomic.Value(usize) align(128) = .init(0),

64バイトではなく128バイト——Apple Silicon(および多くのARM)では、ハードウェアプリフェッチャがキャッシュラインのペアで動作する可能性があるためです。安全策を取っています。

WireCodec:コード生成の代わりにcomptime

WireCodec — コンパイル時コーデック

Java/C++の世界では、バイナリコーデックには別途のステップが必要です:スキーマ作成 → コードジェネレータ実行 → コード取得 → コンパイル。Zigではこれらすべてがコンパイル時に完了します:

const TickMsg = packed struct {
    symbol_id: u32,
    price: i64,
    quantity: u32,
    side: u8,
    _reserved: [3]u8,
    timestamp: u64,
};

const Codec = WireCodec(TickMsg);

// エンコード——32バイトのmemcpyに過ぎない。1-2命令にインライン化。
Codec.encode(&msg, buf[0..Codec.wire_size]);

// デコード——ポインタキャスト。コピーなし。
const tick = Codec.decode(buf[0..Codec.wire_size]);

Zigコンパイラはcomptimeフェーズで以下を検証:

  • 構造体がpackedである(パディングホールなし)
  • サイズが8バイトの倍数(SIMD向けアライメント)
  • すべてのフィールドがプリミティブ型

問題があれば——コンパイルエラーになります。深夜3時の本番環境でのランタイム例外ではありません。

共有メモリによるIPC

2つのプロセスが/dev/shm内の同一ファイルをマップします。パブリッシャーがリングバッファに書き込み、サブスクライバーが読み取ります。ホットパス上にソケットもシステムコールもありません:

// Publisher
const channel = try IpcChannel.create("/market-data", .{
    .term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);

// Subscriber(別プロセス)
const channel = try IpcChannel.open("/market-data", .{
    .term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);

publish()からサブスクライバーのhandler_fn呼び出しまでの全経路——64バイトメッセージで30ナノ秒。

NAKベースのUDP信頼性

ネットワークトランスポートにおいて、ZigBoltはレシーバー駆動の再送メカニズムを使用します。レシーバーがシーケンス番号のギャップをビットマップで追跡し、送信者にNAK(否定確認応答)を送信します。さらにAIMD輻輳制御——TCPライクなスロースタートと輻輳回避——によりネットワークの過負荷を防ぎます。

Raftクラスタ:整合性が必要な場合

メッセージの損失が許されないケース(例:マッチングエンジン)のために、ZigBoltは完全なRaftコンセンサスを搭載しています:

  • リーダー選出:設定可能なタイムアウト(150-300ms)
  • ログレプリケーション ——リーダーが各メッセージをフォロワーに複製
  • WAL(先行書き込みログ):CRC32検証とクラッシュリカバリ対応
  • スナップショット ——WALの無限成長を防止

アーカイブ:記録とリプレイ

すべてのメッセージをディスク上のセグメント化されたアーカイブに記録できます。その後、時間またはシーケンス番号で任意の位置からリプレイ可能。外部依存なしの組み込みLZ4スタイル圧縮。セグメント内の高速検索用スパースインデックス。

トータルオーダーシーケンサ

複数の取引所でのマーケットメイキングでは、すべてのイベントにグローバルな順序が必要です。シーケンサはN個の入力ストリームを受け取り、単調増加するシーケンス番号を割り当てて1つに統合します。すべての参加者が同一のイベントシーケンスを見ることになります。


なぜRust/C/C++ではなくZigなのか?

4つの候補言語で比較しました。以下が公正な比較表です:

基準 Zig C/C++ Rust Java (Aeron)
GC / ランタイムオーバーヘッド なし なし なし JVM safepoints, GC
Comptimeコード生成 ネイティブ マクロ/テンプレート proc macros なし
C相互運用(DPDK, io_uring) 簡単な@cImport ネイティブ FFI/bindgen JNIオーバーヘッド
SIMD @Vector、組み込み Intrinsics packed_simd(不安定) ベクトル化ヒント
クロスコンパイル 組み込み CMake地獄 cargo target N/A
ビルド時間 秒単位 分単位(C++) 分単位 秒単位 + JVM起動
隠れた制御フロー なし 例外、暗黙の型変換 unwrap内のパニック 例外

Zigはユニークな組み合わせを提供しました: Cのパフォーマンス + 開発時の安全性 + comptimeメタプログラミング(コーデック、ルックアップテーブル、プロトコルステートマシン——すべてコンパイル時に生成)+ @cImportによるDPDK、liburing、ef_viとのシームレスな統合。

さらにZigバイナリは約100KB。JVMベースのソリューションの20MB以上と比較してください。エッジデプロイやコンテナにとって、これは重要です。


バインディング:お好みの言語で

ZigBoltはC-ABIの共有ライブラリにコンパイルされ、5つの言語の既製バインディングを提供しています:

TypeScript / Node.js

import { IpcChannel } from "@zigbolt/node";

const channel = IpcChannel.create({
  name: "/my-market-data",
  termLength: 1024 * 1024,
});

const msg = Buffer.from("BTC/USDT 42000.50", "utf-8");
channel.publish(msg, 1);

Rust

use zigbolt::IpcChannel;

let ch = IpcChannel::create("/my-channel", 64 * 1024).unwrap();
ch.publish(b"hello", 1).unwrap();

// Subscriber
let sub = IpcChannel::open("/my-channel", 64 * 1024).unwrap();
sub.poll(|data, msg_type_id| {
    println!("got {} bytes, type={}", data.len(), msg_type_id);
}, 10);

Python

from zigbolt import IpcChannel

ch = IpcChannel.create("/market-data", term_length=1024*1024)
ch.publish(b"tick data here", msg_type_id=1)

GoおよびピュアCバインディングもあります。同一の共有メモリチャネルに、すべての言語から同時にアクセス可能です——パブリッシャーはZig、サブスクライバーはPython、モニタリングはGo。全員が同じmmap領域を読み取ります。


SBEコーデック:FIX互換メッセージ

金融プロトコル向けに、ZigBoltはコンパイル時スキーマによる完全なSBE(Simple Binary Encoding)コーデックを搭載しています。組み込みメッセージタイプ:

  • NewOrderSingle — 注文発注
  • ExecutionReport — 約定報告
  • MarketDataIncrementalRefresh — インクリメンタルマーケットデータ更新
  • MassQuote — 一括クォート
  • Heartbeat — 接続確認
  • Logon — 認証

外部コードジェネレータ不要、XML不要。すべてZig構造体で記述し、コンパイル時に検証されます。


Wire Protocol:Aeronとの互換性

ZigBoltはAeron互換のwire protocol flyweightsを実装しています:

  • DataHeaderFlyweight — データフレーム
  • StatusMessage — フロー制御
  • NAK — 否定確認応答
  • Setup、RTT、Error — 制御フレーム

これは、ZigBoltが既存のAeronインフラストラクチャと共存できることを意味します。移行は一度にすべてを切り替える必要はありません。


今後の展望

ZigBoltは現在バージョン0.2.1です。コアは安定し、ベンチマークは再現可能、バインディングは動作しています。近い将来の計画:

  • io_uringバックエンド — Linux 6.0+でのゼロコピーネットワークトランスポート(IORING_OP_SEND_ZC)
  • DPDK / AF_XDP — 1マイクロ秒が勝負を分けるケースのカーネルバイパス
  • Multi-Raft — 銘柄/戦略別シャーディング
  • カラムナーアーカイブ — 分析のためのApache Arrow/Parquet統合
  • Hugepageサポート — TLBミスを最小化するためのプリフォールト2MB/1GBヒュージページ

試してみる

ソースからビルド(zig build)、ベンチマーク実行(zig build bench)、FFIで任意の言語から接続。Zig 0.15.1と数分の時間があれば——ping-pongベンチマークを試して、現在のソリューションと比較してみてください。


リンク:


引用

@software{soloviov2026zigbolt,
  author = {Soloviov, Eugen},
  title = {ZigBolt: なぜ我々はZigで独自のAeronを構築し、メッセージあたり20ナノ秒を達成したのか},
  year = {2026},
  url = {https://marketmaker.cc/ja/blog/post/zigbolt-zig-messaging-hft},
  version = {0.2.1},
  description = {HFT向け超低レイテンシメッセージングシステムをZig言語でゼロから構築した経緯と理由。JVMなし、GCなし、サプライズなし。}
}
blog.disclaimer

MarketMaker.cc Team

クオンツ・リサーチ&戦略

Telegramで議論する
Newsletter

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。